Introduction
OpenAPI is the most widely adopted standard for describing HTTP APIs. Originally called Swagger (and still often called that informally), it lets you define your API's endpoints, parameters, request bodies, response schemas, and authentication requirements in a single YAML or JSON file. That file becomes the contract between your backend and every client that consumes it — frontend teams, mobile apps, third-party integrators, and automated testing tools all work from the same source of truth.
OpenAPI 3.1 (released in 2021) brought full alignment with JSON Schema draft 2020-12, making schema reuse and validation dramatically more consistent. This guide covers everything you need to design, document, and generate code from an OpenAPI 3.1 specification.
Shortcut: Already have an OpenAPI spec? Use the OpenAPI to Axios Client or OpenAPI to SDK tools to generate TypeScript client code instantly.
Why Design-First Beats Code-First
There are two ways to adopt OpenAPI. In the code-first approach, you write your API implementation and then generate the spec from annotations or decorators in your code. In the design-first approach, you write the spec first, review it with stakeholders, then generate server stubs and client code from it.
Design-first wins for most teams for three reasons:
- Frontend and backend can work in parallel. Once the spec is agreed upon, the frontend team generates a mock server from it and starts building UI. The backend team implements against the same contract. No waiting.
- Breaking changes are caught before any code is written. Renaming a field or changing a response type in the YAML triggers a review conversation, not a production incident.
- Documentation is never out of sync. Code-first annotations drift — developers forget to update them. A design-first spec is the implementation target, so the code must match the spec, not the other way around.
Anatomy of an OpenAPI 3.1 Document
An OpenAPI document is a YAML (or JSON) file with a specific top-level structure. Here is a minimal but complete example for a blog API:
openapi: 3.1.0
info:
title: Blog API
version: 1.0.0
description: |
Manage posts and authors.
Rate limit: 1000 requests per hour per API key.
contact:
name: API Support
email: [email protected]
license:
name: MIT
servers:
- url: https://api.example.com/v1
description: Production
- url: https://staging.example.com/v1
description: Staging
- url: http://localhost:3000/v1
description: Local development
paths:
/posts:
get:
summary: List published posts
operationId: listPosts
tags: [Posts]
parameters:
- $ref: '#/components/parameters/PageParam'
- $ref: '#/components/parameters/LimitParam'
responses:
'200':
description: Paginated list of posts
content:
application/json:
schema:
$ref: '#/components/schemas/PostList'
'401':
$ref: '#/components/responses/Unauthorized'
components:
schemas: {} # defined below
parameters: {} # defined below
responses: {} # defined below
securitySchemes: {} # defined belowThe top-level sections and their purpose:
| Section | Required | Purpose |
|---|---|---|
| openapi | Yes | Version string — must be "3.1.0" (or "3.0.x" for older) |
| info | Yes | API title, version, description, contact, license |
| servers | No | Base URLs for the API (production, staging, local) |
| paths | Yes* | All endpoints — each path maps to operations (GET, POST, etc.) |
| components | No | Reusable schemas, parameters, responses, security schemes |
| security | No | Global security requirement applied to all paths by default |
| tags | No | Group operations for documentation and SDK organization |
| externalDocs | No | Link to external documentation |
* Either paths or webhooks must be present.
Defining Paths and Operations
The paths object is the heart of your spec. Each key is a URL path (with optional path parameters in curly braces), and each nested key is an HTTP method.
paths:
/posts/{postId}:
get:
summary: Get a post by ID
operationId: getPost # unique identifier — used in code generation
tags: [Posts]
security:
- BearerAuth: [] # override global security for this operation
parameters:
- name: postId
in: path
required: true
schema:
type: integer
minimum: 1
description: Numeric ID of the post
responses:
'200':
description: The requested post
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
example:
id: 42
title: "OpenAPI Guide"
published: true
'404':
description: Post not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
patch:
summary: Update a post
operationId: updatePost
tags: [Posts]
parameters:
- name: postId
in: path
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdatePostInput'
responses:
'200':
description: Updated post
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
'422':
$ref: '#/components/responses/ValidationError'
delete:
summary: Delete a post
operationId: deletePost
tags: [Posts]
parameters:
- name: postId
in: path
required: true
schema: { type: integer }
responses:
'204':
description: Post deleted — no response body
'404':
$ref: '#/components/responses/NotFound'Key things to get right on every operation:
- operationId — unique across the entire spec, used as the function name in generated clients. Use camelCase verbs:
listPosts,createPost,updatePost,deletePost. - tags — group related operations. SDK generators typically create one class or module per tag.
- Always document error responses — at minimum 401, 404, and 422. Clients need to know the error shape to handle them correctly.
- Use $ref everywhere you can — defining schemas inline is fine for one-offs, but any schema used in more than one place belongs in
components.
Schema Objects: Typing Your Data
OpenAPI 3.1 schema objects are a superset of JSON Schema. You define the shape of every request body and response in components/schemas, then reference them with$ref. Here is the schema set for the blog API:
components:
schemas:
Post:
type: object
required: [id, title, content, published, authorId, createdAt]
properties:
id:
type: integer
readOnly: true # never required in write operations
title:
type: string
minLength: 1
maxLength: 200
content:
type: string
published:
type: boolean
default: false
views:
type: integer
minimum: 0
default: 0
readOnly: true
authorId:
type: integer
tags:
type: array
items:
type: string
createdAt:
type: string
format: date-time
readOnly: true
deletedAt:
type: string
format: date-time
nullable: true # 3.0 syntax — in 3.1 use: type: [string, 'null']
CreatePostInput:
type: object
required: [title, content, authorId]
properties:
title:
type: string
minLength: 1
maxLength: 200
content:
type: string
tags:
type: array
items:
type: string
default: []
UpdatePostInput:
type: object
properties:
title:
type: string
minLength: 1
maxLength: 200
content:
type: string
published:
type: boolean
tags:
type: array
items:
type: string
PostList:
type: object
required: [data, total, page, limit]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Post'
total:
type: integer
page:
type: integer
limit:
type: integer
Error:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string
details:
type: object
additionalProperties: trueOpenAPI scalar types and their TypeScript equivalents when generating client code:
| OpenAPI type + format | TypeScript type | Notes |
|---|---|---|
| type: integer | number | 32-bit integer |
| type: integer, format: int64 | number | string | May exceed JS safe integer range |
| type: number | number | 64-bit float |
| type: string | string | |
| type: string, format: date | string | ISO 8601 date — generators may use Date |
| type: string, format: date-time | string | ISO 8601 datetime — generators may use Date |
| type: string, format: uuid | string | Validated as UUID by some validators |
| type: string, format: binary | Blob | File | Used for file uploads |
| type: boolean | boolean | |
| type: array, items: $ref | T[] | T is the referenced schema type |
| type: object | Record<string, unknown> | Without properties defined |
| enum: [a, b, c] | "a" | "b" | "c" | String literal union |
Composition: allOf, anyOf, oneOf
OpenAPI 3.1 inherits JSON Schema's composition keywords. These map directly to TypeScript intersection and union types when generating code:
# allOf: intersection — type must satisfy ALL listed schemas
# TypeScript: type A = Base & Extended
AdminPost:
allOf:
- $ref: '#/components/schemas/Post'
- type: object
required: [editedBy]
properties:
editedBy:
type: integer
# oneOf: exactly ONE of the schemas — discriminated union
# TypeScript: type Notification = EmailNotif | PushNotif
Notification:
oneOf:
- $ref: '#/components/schemas/EmailNotification'
- $ref: '#/components/schemas/PushNotification'
discriminator:
propertyName: type
mapping:
email: '#/components/schemas/EmailNotification'
push: '#/components/schemas/PushNotification'
# anyOf: one OR more schemas — use sparingly (hard to validate)
FlexibleInput:
anyOf:
- type: string
- type: integerParameters and Reusable Components
Parameters appear in four locations: path, query,header, and cookie. Define them once incomponents/parameters and reference them everywhere:
components:
parameters:
PageParam:
name: page
in: query
required: false
schema:
type: integer
minimum: 1
default: 1
description: Page number for pagination
LimitParam:
name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
description: Results per page
SortParam:
name: sort
in: query
schema:
type: string
enum: [created_at, views, title]
default: created_at
responses:
Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: UNAUTHORIZED
message: "Bearer token missing or invalid"
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ValidationError:
description: Request body failed validation
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Error'
- type: object
properties:
fields:
type: object
additionalProperties:
type: array
items:
type: string
example:
code: VALIDATION_ERROR
message: "Request validation failed"
fields:
title: ["must not be empty"]
authorId: ["must be a positive integer"]Security Schemes
Define your authentication method in components/securitySchemes, then apply it globally or per-operation. Generated clients will include the correct headers or parameters automatically.
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT # informational only — not validated
description: |
JWT obtained from POST /auth/token.
Include as: Authorization: Bearer <token>
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: API key for server-to-server calls
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/oauth/authorize
tokenUrl: https://auth.example.com/oauth/token
scopes:
posts:read: Read published posts
posts:write: Create and update posts
posts:delete: Delete posts
# Apply BearerAuth to all operations by default
security:
- BearerAuth: []
# Override for specific operations — public endpoint, no auth
paths:
/posts:
get:
security: [] # empty array = no security requiredGenerating Client Code from Your Spec
This is where the investment in writing a good spec pays off. A single spec file generates typed clients for every language and framework your consumers use.
TypeScript Axios client
Use the OpenAPI to Axios Client tool to paste your spec and get a typed Axios client back instantly. The generated code creates one function per operationId:
// Generated from operationId: getPost
export async function getPost(postId: number): Promise<Post> {
const { data } = await axios.get<Post>(`/posts/${postId}`);
return data;
}
// Generated from operationId: createPost
export async function createPost(body: CreatePostInput): Promise<Post> {
const { data } = await axios.post<Post>('/posts', body);
return data;
}
// Generated from operationId: listPosts
export async function listPosts(params?: {
page?: number;
limit?: number;
}): Promise<PostList> {
const { data } = await axios.get<PostList>('/posts', { params });
return data;
}Python client
The OpenAPI to Python Client tool generates httpx-based async functions with Pydantic models for request and response types — ready to drop into a FastAPI or Django project.
Validation in Node.js
Use openapi-fetch (from the openapi-typescript ecosystem) for the tightest type-safe integration. It infers request/response types directly from the spec without a code generation step:
import createClient from 'openapi-fetch';
import type { paths } from './openapi'; // generated by openapi-typescript
const client = createClient<paths>({ baseUrl: 'https://api.example.com/v1' });
// TypeScript knows exact parameter and response types
const { data, error } = await client.GET('/posts/{postId}', {
params: { path: { postId: 42 } },
});
// data is Post | undefined, error is Error | undefined
if (error) {
console.error(error.message);
} else {
console.log(data.title); // TypeScript knows this is string
}OpenAPI vs GraphQL vs gRPC: When to Choose Each
All three are serious API technologies used at scale. The decision is not about which is "better" — it's about which properties matter most for your project.
Choose OpenAPI (REST) when:
- You need a public API that third-party developers can consume without learning a new query language
- Your API is consumed from browsers, mobile apps, or shell scripts — HTTP is the universal denominator
- You want the widest possible tooling ecosystem: API gateways, mocking tools, documentation renderers (Redoc, Swagger UI), and testing frameworks all speak OpenAPI natively
- Caching matters — HTTP GET responses are cacheable by CDNs and browsers without extra configuration
- You are building CRUD-heavy APIs where resources map cleanly to URLs
Choose GraphQL when:
- Clients need to fetch complex, nested, or highly variable data shapes in a single request
- You have many different client types (web, mobile, partner) each needing different subsets of the same data
- You want to avoid over-fetching and under-fetching without building per-client REST endpoints
- Your team is building a single internal API consumed by your own frontends, not a public API
Choose gRPC when:
- You are building service-to-service communication in a microservices backend (not browser-facing)
- Payload efficiency and throughput are critical — Protocol Buffers are 3–10x smaller than equivalent JSON
- You need bidirectional streaming (gRPC supports client streaming, server streaming, and full duplex)
- Your team already uses proto files as the canonical schema for multiple polyglot services
| Factor | OpenAPI / REST | GraphQL | gRPC |
|---|---|---|---|
| Transport | HTTP/1.1 or HTTP/2 | HTTP/1.1 or HTTP/2 | HTTP/2 only |
| Schema format | YAML / JSON (OpenAPI spec) | GraphQL SDL | Protocol Buffers (.proto) |
| Browser support | Native | Yes (via Apollo/urql) | Limited (needs grpc-web) |
| Payload format | JSON (or any) | JSON | Binary (Protobuf) |
| Caching | Native HTTP caching | Manual (persisted queries) | None by default |
| Streaming | SSE / WebSocket addon | Subscriptions | Native bidirectional |
| Code generation | Excellent (many generators) | Good (codegen) | Excellent (protoc) |
| Learning curve | Low (HTTP + JSON) | Medium | High |
| Best for | Public APIs, CRUD, mobile | Complex data, internal | Microservices, high perf |
Practical Tooling Tips
Validate your spec before generating
A spec with schema errors produces broken generated code. Run validation before any generation step:
# Redocly CLI (recommended — best OpenAPI 3.1 support) npx @redocly/cli lint openapi.yaml # Stoplight Spectral (highly configurable rulesets) npx @stoplight/spectral-cli lint openapi.yaml # openapi-typescript (generates TypeScript types from spec) npx openapi-typescript openapi.yaml -o src/openapi.d.ts
Mock server from spec
Run a mock API server from your spec so frontend developers can work before the backend exists:
# Prism mock server — serves example values from your spec npx @stoplight/prism-cli mock openapi.yaml --port 4010 # Redocly mock server (validates requests against spec too) npx @redocly/cli preview-docs openapi.yaml
Convert Postman collections to OpenAPI
If your team already has a Postman collection and wants to migrate to an OpenAPI-first workflow, the Swagger to Postman tool goes in the other direction — from an OpenAPI spec to a Postman collection that can be imported and used for manual testing immediately.
Related Tools on ByteJSON
Written by Zhisan
Independent Developer · Last updated June 2026