If you've ever integrated with a poorly documented API, you know the pain. Hours spent guessing what fields are required, what data types to expect, and what error codes mean. OpenAPI exists to solve that problem — it's a standard way to describe REST APIs so both humans and machines can understand them.
OpenAPI 3.1 is the latest version, and it's a significant improvement over 3.0. Let's walk through everything you need to know to write specs that are actually useful.
What Is OpenAPI?
OpenAPI (formerly Swagger) is a specification for describing REST APIs. An OpenAPI document is a JSON or YAML file that describes:
- What endpoints your API has
- What parameters each endpoint accepts
- What request bodies look like
- What responses to expect
- How authentication works
Here's the simplest possible OpenAPI document:
openapi: 3.1.0
info:
title: PetStore API
version: 1.0.0
paths:
/pets:
get:
summary: List all pets
responses:
'200':
description: A list of pets
That's valid OpenAPI. Now let's build something more complete.
Document Structure
An OpenAPI 3.1 document has these top-level fields:
openapi: 3.1.0 # Required: spec version
info: ... # Required: API metadata
servers: ... # Optional: server URLs
paths: ... # Required: API endpoints
components: ... # Optional: reusable definitions
security: ... # Optional: global security
tags: ... # Optional: endpoint grouping
externalDocs: ... # Optional: external documentation
The info Object
info:
title: PetStore API
description: |
A sample API for managing a pet store.
## Authentication
All endpoints require a valid API key passed in the `X-API-Key` header.
## Rate Limiting
Requests are limited to 100 per minute per API key.
version: 2.1.0
contact:
name: PetStore Support
email: support@petstore.com
url: https://petstore.com/support
license:
name: MIT
url: https://opensource.org/licenses/MIT
termsOfService: https://petstore.com/terms
The servers Object
servers:
- url: https://api.petstore.com/v2
description: Production server
- url: https://staging-api.petstore.com/v2
description: Staging server
- url: http://localhost:3000/v2
description: Local development
You can also use server variables for dynamic URLs:
servers:
- url: https://{region}.api.petstore.com/v2
description: Regional API server
variables:
region:
default: us-east
enum:
- us-east
- us-west
- eu-central
description: The geographic region for the API server
Defining Paths
Paths are the core of your OpenAPI spec. Each path maps to an endpoint, and each endpoint can have multiple HTTP methods.
paths:
/pets:
get:
operationId: listPets
summary: List all pets
description: Returns a paginated list of all pets in the store.
tags:
- Pets
parameters:
- name: limit
in: query
description: Maximum number of pets to return
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: status
in: query
description: Filter by pet status
schema:
type: string
enum:
- available
- pending
- sold
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/PetList'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
security:
- ApiKeyAuth: []
post:
operationId: createPet
summary: Create a new pet
tags:
- Pets
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePetRequest'
examples:
dog:
summary: A dog example
value:
name: Rex
species: dog
breed: German Shepherd
age: 3
status: available
cat:
summary: A cat example
value:
name: Whiskers
species: cat
age: 5
status: available
responses:
'201':
description: Pet created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
headers:
Location:
description: URL of the newly created pet
schema:
type: string
format: uri
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
/pets/{petId}:
parameters:
- name: petId
in: path
required: true
description: The unique identifier of the pet
schema:
type: integer
format: int64
minimum: 1
get:
operationId: getPet
summary: Get a pet by ID
tags:
- Pets
responses:
'200':
description: Pet found
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
'404':
$ref: '#/components/responses/NotFound'
put:
operationId: updatePet
summary: Update a pet
tags:
- Pets
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdatePetRequest'
responses:
'200':
description: Pet updated
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
'404':
$ref: '#/components/responses/NotFound'
delete:
operationId: deletePet
summary: Delete a pet
tags:
- Pets
responses:
'204':
description: Pet deleted successfully
'404':
$ref: '#/components/responses/NotFound'
Components: Reusable Definitions
The components section is where you define reusable pieces. This keeps your spec DRY and easier to maintain.
Schemas
Schemas define the shape of your data. OpenAPI 3.1 uses JSON Schema draft 2020-12, which gives you a lot of power.
components:
schemas:
Pet:
type: object
required:
- id
- name
- species
- status
properties:
id:
type: integer
format: int64
readOnly: true
description: Unique identifier for the pet
examples:
- 42
name:
type: string
minLength: 1
maxLength: 100
description: The pet's name
examples:
- Rex
species:
type: string
enum:
- dog
- cat
- bird
- fish
- reptile
- other
description: The type of animal
breed:
type: string
nullable: true
description: The breed (if applicable)
age:
type: integer
minimum: 0
maximum: 100
description: Age in years
weight:
type: number
format: float
minimum: 0
description: Weight in kilograms
status:
$ref: '#/components/schemas/PetStatus'
tags:
type: array
items:
type: string
uniqueItems: true
description: Searchable tags for the pet
photos:
type: array
items:
$ref: '#/components/schemas/Photo'
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
PetStatus:
type: string
enum:
- available
- pending
- sold
description: The current availability status of the pet
CreatePetRequest:
type: object
required:
- name
- species
properties:
name:
type: string
minLength: 1
maxLength: 100
species:
type: string
enum:
- dog
- cat
- bird
- fish
- reptile
- other
breed:
type: string
age:
type: integer
minimum: 0
weight:
type: number
minimum: 0
status:
$ref: '#/components/schemas/PetStatus'
default: available
tags:
type: array
items:
type: string
UpdatePetRequest:
type: object
minProperties: 1
properties:
name:
type: string
minLength: 1
maxLength: 100
breed:
type: string
age:
type: integer
minimum: 0
weight:
type: number
minimum: 0
status:
$ref: '#/components/schemas/PetStatus'
tags:
type: array
items:
type: string
PetList:
type: object
required:
- data
- pagination
properties:
data:
type: array
items:
$ref: '#/components/schemas/Pet'
pagination:
$ref: '#/components/schemas/Pagination'
Pagination:
type: object
required:
- total
- page
- pageSize
- totalPages
properties:
total:
type: integer
description: Total number of items
page:
type: integer
description: Current page number
pageSize:
type: integer
description: Number of items per page
totalPages:
type: integer
description: Total number of pages
nextPage:
type: string
format: uri
nullable: true
description: URL for the next page
prevPage:
type: string
format: uri
nullable: true
description: URL for the previous page
Photo:
type: object
required:
- url
properties:
url:
type: string
format: uri
caption:
type: string
isPrimary:
type: boolean
default: false
Error:
type: object
required:
- code
- message
properties:
code:
type: string
description: Machine-readable error code
message:
type: string
description: Human-readable error message
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
requestId:
type: string
description: Request ID for debugging
Using Composition with allOf, oneOf, anyOf
OpenAPI 3.1 supports JSON Schema composition keywords:
schemas:
# allOf: must match ALL schemas
PremiumPet:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
premiumFeatures:
type: array
items:
type: string
insurancePolicy:
type: string
# oneOf: must match EXACTLY ONE schema
AnimalIdentifier:
oneOf:
- type: object
required:
- petId
properties:
petId:
type: integer
- type: object
required:
- microchipId
properties:
microchipId:
type: string
# anyOf: must match AT LEAST ONE schema
SearchFilter:
anyOf:
- type: object
properties:
species:
type: string
- type: object
properties:
ageRange:
type: object
properties:
min:
type: integer
max:
type: integer
Reusable Responses
responses:
BadRequest:
description: Invalid request parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: VALIDATION_ERROR
message: Request validation failed
details:
- field: name
message: Name is required
requestId: req_abc123
Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: UNAUTHORIZED
message: Valid API key required
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: NOT_FOUND
message: Pet with ID 999 not found
RateLimited:
description: Too many requests
headers:
X-RateLimit-Limit:
schema:
type: integer
description: Request limit per minute
X-RateLimit-Remaining:
schema:
type: integer
description: Remaining requests in current window
X-RateLimit-Reset:
schema:
type: integer
description: Unix timestamp when the window resets
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: RATE_LIMITED
message: Too many requests. Please slow down.
Reusable Parameters
parameters:
PetIdParam:
name: petId
in: path
required: true
schema:
type: integer
format: int64
minimum: 1
description: The unique identifier of the pet
LimitParam:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
description: Maximum number of results to return
PageParam:
name: page
in: query
schema:
type: integer
minimum: 1
default: 1
description: Page number for pagination
Security Schemes
OpenAPI 3.1 supports several authentication types.
API Key Authentication
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: |
API key for authentication. Get your key at https://petstore.com/dashboard.
Example: `X-API-Key: pk_live_abc123`
Bearer Token (JWT)
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT token obtained from the /auth/token endpoint.
Example: `Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...`
OAuth 2.0
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.petstore.com/oauth/authorize
tokenUrl: https://auth.petstore.com/oauth/token
refreshUrl: https://auth.petstore.com/oauth/refresh
scopes:
pets:read: Read pet information
pets:write: Create and update pets
pets:delete: Delete pets
orders:read: Read order information
orders:write: Create and manage orders
clientCredentials:
tokenUrl: https://auth.petstore.com/oauth/token
scopes:
pets:read: Read pet information
pets:write: Create and update pets
Apply security globally or per-endpoint:
# Global security (applies to all endpoints)
security:
- ApiKeyAuth: []
# Override per endpoint
paths:
/public/pets:
get:
security: [] # No auth required for this endpoint
/pets:
post:
security:
- OAuth2:
- pets:write # Requires specific OAuth scope
Writing Good OpenAPI Specs
Use operationId Consistently
Every operation should have a unique operationId. Use camelCase and follow a consistent naming pattern:
# Good
operationId: listPets
operationId: createPet
operationId: getPetById
operationId: updatePet
operationId: deletePet
# Bad
operationId: GET_pets
operationId: pets_post
operationId: operation1
Add Meaningful Examples
Examples make your spec much more useful:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePetRequest'
examples:
minimal:
summary: Minimal required fields
value:
name: Buddy
species: dog
complete:
summary: All fields provided
value:
name: Buddy
species: dog
breed: Labrador Retriever
age: 2
weight: 28.5
status: available
tags:
- friendly
- trained
- vaccinated
Document Error Responses Thoroughly
Don't just list 200 responses. Document every error your API can return:
responses:
'200':
description: Success
'400':
description: |
Bad request. Common causes:
- Missing required fields
- Invalid field values
- Malformed JSON
'401':
description: Missing or invalid API key
'403':
description: Valid API key but insufficient permissions
'404':
description: Pet not found
'409':
description: Conflict — pet with this name already exists
'422':
description: Validation error — request is well-formed but semantically invalid
'429':
description: Rate limit exceeded
'500':
description: Internal server error — please contact support
Use $ref to Avoid Repetition
If you find yourself copying the same schema in multiple places, extract it into components:
# Instead of this (repeated in every endpoint):
responses:
'401':
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
code:
type: string
message:
type: string
# Do this:
responses:
'401':
$ref: '#/components/responses/Unauthorized'
Validation Tools
Spectral
Spectral is a powerful linter for OpenAPI specs. It catches common mistakes and enforces style rules.
Install and run:
npm install -g @stoplight/spectral-cli
spectral lint openapi.yaml
Create a custom ruleset:
# .spectral.yaml
extends: spectral:oas
rules:
operation-operationId: error
operation-summary: error
operation-description: warn
info-contact: warn
# Custom rule: all responses must have examples
response-examples:
description: All responses should have examples
given: "$.paths[*][*].responses[*].content[*]"
severity: warn
then:
field: examples
function: truthy
openapi-validator
IBM's OpenAPI validator is thorough and catches issues Spectral misses:
npm install -g ibm-openapi-validator
lint-openapi openapi.yaml
Redocly CLI
Redocly offers both linting and bundling:
npm install -g @redocly/cli
redocly lint openapi.yaml
redocly bundle openapi.yaml --output bundled.yaml
Swagger Editor
For a visual editing experience, use the Swagger Editor online at editor.swagger.io or run it locally:
docker run -p 8080:8080 swaggerapi/swagger-editor
Generating Code from OpenAPI
One of the best things about OpenAPI is that you can generate client SDKs and server stubs automatically.
Generate a TypeScript client
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o ./src/generated/api-client
Generate a Python client
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g python \
-o ./clients/python
Generate server stubs
# Express.js server stub
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g nodejs-express-server \
-o ./server-stub
Keeping Your Spec in Sync
The biggest challenge with OpenAPI is keeping the spec in sync with your actual API. Here are some strategies:
Code-first with annotations: Generate the spec from code comments
/**
* @openapi
* /pets:
* get:
* summary: List all pets
* responses:
* 200:
* description: Success
*/
app.get('/pets', listPets);
Spec-first: Write the spec first, then implement it (covered in the API-First Development article)
Contract testing: Use tools like Dredd or Schemathesis to verify your implementation matches the spec
# Test your API against the spec
schemathesis run openapi.yaml --url http://localhost:3000
Wrapping Up
A well-written OpenAPI spec is one of the most valuable things you can create for your API. It serves as documentation, enables code generation, powers mock servers, and makes testing easier.
The key is to be thorough without being verbose. Document every parameter, every response, every error. Use $ref to keep things DRY. Add examples that show real-world usage. And validate your spec with tools like Spectral to catch mistakes early.
Start with the basics — paths, schemas, and responses — and add more detail over time. Even an incomplete spec is better than no spec at all.