What This Template Is For
An OpenAPI specification is a machine-readable description of your REST API. It defines every endpoint, request parameter, response schema, authentication method, and error format in a YAML or JSON file that tools can consume directly. Swagger UI generates interactive documentation from it. Code generators build client SDKs. Contract testing tools validate that your implementation matches the spec.
Writing the OpenAPI spec before implementation prevents two common problems. First, it creates a single source of truth that frontend engineers, QA, partner teams, and documentation tools all reference. Second, it forces you to design the complete API surface, including edge cases and error responses, before writing any handler code.
This template provides a structured approach to writing an OpenAPI 3.1 specification. It covers the info, servers, paths, components, and security sections with guidance on when and how to use each feature. If you need to design the API at a higher level before writing the formal spec, start with the API Design Specification Template. For how API documentation fits into the developer experience, see the Technical PM Handbook.
To understand how this spec connects to API-first design principles, review the glossary entry.
How to Use This Template
- Start by filling out the
infosection with your API's name, version, and description. This is the first thing consumers see in generated documentation. - Define the
serverssection with your base URLs for production, staging, and sandbox environments. - Design your schemas first (in the
components/schemassection). Define every request body, response body, and shared type before writing path definitions. - Write path definitions for each endpoint. Reference schemas from
components/schemasrather than inlining them. - Define security schemes in
components/securitySchemesand apply them globally or per-endpoint. - Document error responses with a shared error schema. Every endpoint should reference the same error format.
- Validate the spec with a linter (like Spectral or
redocly lint) before sharing with consumers.
The Template
Info Section
openapi: "3.1.0"
info:
title: "[API Name]"
version: "[Semantic version, e.g., 1.0.0]"
description: |
[2-4 sentences describing what this API does, who it is for,
and what the primary use cases are.]
contact:
name: "[Support team or person]"
email: "[Support email]"
url: "[Developer docs URL]"
license:
name: "[License name]"
url: "[License URL]"
x-api-id: "[Unique identifier for this API, e.g., UUID]"
Info section checklist:
- ☐ Title is clear and matches the product name consumers know
- ☐ Version follows semantic versioning (MAJOR.MINOR.PATCH)
- ☐ Description explains what the API does in plain language
- ☐ Contact info points to a real support channel
- ☐ License is specified for external APIs
Servers
servers:
- url: "https://api.example.com/v1"
description: "Production"
- url: "https://api.staging.example.com/v1"
description: "Staging"
- url: "https://api.sandbox.example.com/v1"
description: "Sandbox (test data, no real charges)"
Server configuration notes:
- ☐ Production URL uses HTTPS
- ☐ Staging URL is accessible with test credentials
- ☐ Sandbox environment resets data nightly (document this)
- ☐ Version is included in the base URL path (if using URL versioning)
Security Schemes
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
[How to obtain a token. Link to auth documentation.]
apiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: |
[How to obtain an API key. Link to dashboard.]
oauth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: "https://auth.example.com/authorize"
tokenUrl: "https://auth.example.com/token"
refreshUrl: "https://auth.example.com/token"
scopes:
"read:resources": "Read access to resources"
"write:resources": "Write access to resources"
"admin": "Full admin access"
# Apply globally (override per-endpoint if needed)
security:
- bearerAuth: []
Shared Schemas
components:
schemas:
# Standard error response
Error:
type: object
required:
- error
properties:
error:
type: object
required:
- code
- message
properties:
code:
type: string
description: "Machine-readable error code"
example: "VALIDATION_ERROR"
message:
type: string
description: "Human-readable error description"
example: "The 'email' field is required"
details:
type: array
items:
type: object
properties:
field:
type: string
issue:
type: string
doc_url:
type: string
format: uri
description: "Link to error documentation"
# Pagination wrapper
PaginatedResponse:
type: object
properties:
data:
type: array
items: {}
pagination:
$ref: "#/components/schemas/Pagination"
Pagination:
type: object
properties:
cursor:
type: string
nullable: true
has_more:
type: boolean
total_count:
type: integer
# Resource schemas
"[ResourceName]":
type: object
required:
- id
- "[required_field]"
properties:
id:
type: string
format: uuid
readOnly: true
description: "Unique identifier"
"[field_name]":
type: string
description: "[What this field represents]"
example: "[Example value]"
created_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
"Create[ResourceName]Request":
type: object
required:
- "[required_field]"
properties:
"[field_name]":
type: string
description: "[What this field represents]"
minLength: 1
maxLength: 255
"Update[ResourceName]Request":
type: object
properties:
"[field_name]":
type: string
description: "[What this field represents]"
minLength: 1
maxLength: 255
Schema design checklist:
- ☐ All request schemas have validation constraints (minLength, maxLength, minimum, maximum, pattern)
- ☐ All response schemas include
examplevalues for documentation - ☐ Read-only fields (id, created_at, updated_at) are marked with
readOnly: true - ☐ Nullable fields use
nullable: true(nottype: ["string", "null"]) - ☐ Enums list all valid values with descriptions
- ☐ Shared schemas are defined in
components/schemasand referenced via$ref
Path Definitions
paths:
"/[resources]":
get:
summary: "List [resources]"
operationId: "list[Resources]"
tags:
- "[Resource Group]"
description: |
[What this endpoint does. Mention pagination, filtering, and sorting.]
parameters:
- name: first
in: query
schema:
type: integer
default: 20
maximum: 100
description: "Number of results to return (max 100)"
- name: after
in: query
schema:
type: string
description: "Cursor for pagination"
- name: filter
in: query
schema:
type: string
description: "[Available filter fields and syntax]"
- name: sort
in: query
schema:
type: string
enum: ["created_at", "-created_at", "name", "-name"]
description: "Sort field (prefix with - for descending)"
responses:
"200":
description: "Successful response"
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginatedResponse"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/[ResourceName]"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
post:
summary: "Create a [resource]"
operationId: "create[Resource]"
tags:
- "[Resource Group]"
description: |
[What this endpoint does. Mention required fields and side effects.]
parameters:
- name: Idempotency-Key
in: header
required: true
schema:
type: string
format: uuid
description: "Unique key for idempotent request handling"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Create[ResourceName]Request"
responses:
"201":
description: "Resource created"
content:
application/json:
schema:
$ref: "#/components/schemas/[ResourceName]"
"400":
$ref: "#/components/responses/ValidationError"
"401":
$ref: "#/components/responses/Unauthorized"
"409":
$ref: "#/components/responses/Conflict"
"429":
$ref: "#/components/responses/RateLimited"
"/[resources]/{id}":
get:
summary: "Get a [resource]"
operationId: "get[Resource]"
tags:
- "[Resource Group]"
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: "Successful response"
content:
application/json:
schema:
$ref: "#/components/schemas/[ResourceName]"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
patch:
summary: "Update a [resource]"
operationId: "update[Resource]"
tags:
- "[Resource Group]"
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Update[ResourceName]Request"
responses:
"200":
description: "Resource updated"
content:
application/json:
schema:
$ref: "#/components/schemas/[ResourceName]"
"400":
$ref: "#/components/responses/ValidationError"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
delete:
summary: "Delete a [resource]"
operationId: "delete[Resource]"
tags:
- "[Resource Group]"
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
"204":
description: "Resource deleted"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
Shared Responses
components:
responses:
ValidationError:
description: "Request validation failed"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error:
code: "VALIDATION_ERROR"
message: "Request validation failed"
details:
- field: "email"
issue: "Must be a valid email address"
Unauthorized:
description: "Authentication required"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error:
code: "UNAUTHORIZED"
message: "Invalid or missing authentication token"
NotFound:
description: "Resource not found"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error:
code: "NOT_FOUND"
message: "The requested resource does not exist"
Conflict:
description: "Resource conflict"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error:
code: "CONFLICT"
message: "A resource with this identifier already exists"
RateLimited:
description: "Too many requests"
headers:
Retry-After:
schema:
type: integer
description: "Seconds until the rate limit resets"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
error:
code: "RATE_LIMITED"
message: "Rate limit exceeded. Retry after 60 seconds."
Tags
tags:
- name: "[Resource Group 1]"
description: "[What this group of endpoints covers]"
- name: "[Resource Group 2]"
description: "[What this group of endpoints covers]"
OpenAPI Spec Validation Checklist
- ☐ Spec passes
redocly lintor Spectral with zero errors - ☐ Every path has at least one response defined
- ☐ Every 4xx/5xx response uses the shared Error schema
- ☐ Every list endpoint has pagination parameters
- ☐ Every write endpoint documents the Idempotency-Key header
- ☐ All schemas have examples for documentation rendering
- ☐ operationId values are unique across the spec
- ☐ Tags are used consistently to group related endpoints
Open Questions
| # | Question | Owner | Status | Decision |
|---|---|---|---|---|
| 1 | [Question] | [Name] | Open | |
| 2 | [Question] | [Name] | Open |
Filled Example: TaskFlow API v1
openapi: "3.1.0"
info:
title: "TaskFlow API"
version: "1.0.0"
description: |
The TaskFlow API enables third-party integrations and internal clients
to create, read, update, and manage projects and tasks. Primary consumers
are the web/mobile frontend, Zapier integration, and 3 partner applications.
contact:
name: "TaskFlow Developer Support"
email: "api-support@taskflow.io"
url: "https://docs.taskflow.io"
servers:
- url: "https://api.taskflow.io/v1"
description: "Production"
- url: "https://api.sandbox.taskflow.io/v1"
description: "Sandbox (test data, resets daily)"
paths:
/tasks:
get:
summary: "List tasks"
operationId: "listTasks"
tags: ["Tasks"]
parameters:
- name: project_id
in: query
required: true
schema:
type: string
format: uuid
- name: status
in: query
schema:
type: string
enum: ["todo", "in_progress", "done"]
- name: first
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
"200":
description: "List of tasks"
This excerpt shows the pattern. A complete TaskFlow spec would include 15-20 endpoints across Tasks, Projects, and Users resource groups.
Key Takeaways
- Write the OpenAPI spec before implementation to use it as a design document and contract
- Define schemas in
components/schemasand reference them via$refto avoid duplication - Document every error response with a consistent format and example values
- Validate the spec with a linter in CI to catch structural issues before they reach consumers
- Use contract testing to prevent drift between the spec and the implementation
About This Template
Created by: Tim Adair
Last Updated: 3/5/2026
Version: 1.0.0
License: Free for personal and commercial use
