OpenAPI
OpenAPI is the standard language for describing HTTP APIs. A single OpenAPI document captures your endpoints, request parameters, request bodies, response shapes, authentication schemes, and error formats. That document becomes the foundation for documentation, client generation, server stubs, mock servers, and contract tests.
The current version of the specification is OpenAPI 3.1, which aligns with JSON Schema 2020-12. Earlier 3.0.x versions are still widely used and supported by most tooling. This guide focuses on patterns that work in both versions unless noted otherwise.
This guide covers the structure of an OpenAPI document, how to generate it from code, how to render interactive documentation, how to generate typed clients, and how to keep the specification and implementation in sync.
An OpenAPI document has four major sections. The info block describes metadata such as title, version, and contact details. The paths block defines every endpoint. The components block holds reusable schemas, parameters, and security schemes. The security block applies default authentication requirements.
| 1 | openapi: 3.0.3 |
| 2 | info: |
| 3 | title: ForgeLearn Orders API |
| 4 | version: 1.2.0 |
| 5 | description: | |
| 6 | Manage orders, customers, and products for the ForgeLearn platform. |
| 7 | contact: |
| 8 | name: API Support |
| 9 | email: api@forgelearn.dev |
| 10 | license: |
| 11 | name: MIT |
| 12 | url: https://opensource.org/licenses/MIT |
| 13 | |
| 14 | servers: |
| 15 | - url: https://api.forgelearn.dev/v1 |
| 16 | description: Production |
| 17 | - url: https://api-staging.forgelearn.dev/v1 |
| 18 | description: Staging |
| 19 | |
| 20 | paths: |
| 21 | /orders: |
| 22 | get: |
| 23 | operationId: listOrders |
| 24 | summary: List orders |
| 25 | tags: [Orders] |
| 26 | parameters: |
| 27 | - $ref: "#/components/parameters/PageCursor" |
| 28 | - $ref: "#/components/parameters/PageLimit" |
| 29 | responses: |
| 30 | "200": |
| 31 | $ref: "#/components/responses/OrderListResponse" |
| 32 | post: |
| 33 | operationId: createOrder |
| 34 | summary: Create an order |
| 35 | tags: [Orders] |
| 36 | requestBody: |
| 37 | required: true |
| 38 | content: |
| 39 | application/json: |
| 40 | schema: |
| 41 | $ref: "#/components/schemas/CreateOrderRequest" |
| 42 | responses: |
| 43 | "201": |
| 44 | $ref: "#/components/responses/OrderResponse" |
| 45 | "422": |
| 46 | $ref: "#/components/responses/ValidationError" |
| 47 | |
| 48 | components: |
| 49 | schemas: |
| 50 | Order: |
| 51 | type: object |
| 52 | required: [id, customer_id, status, total] |
| 53 | properties: |
| 54 | id: |
| 55 | type: string |
| 56 | format: uuid |
| 57 | customer_id: |
| 58 | type: string |
| 59 | format: uuid |
| 60 | status: |
| 61 | type: string |
| 62 | enum: [pending, paid, shipped, cancelled] |
| 63 | total: |
| 64 | type: integer |
| 65 | description: Amount in smallest currency unit |
| 66 | created_at: |
| 67 | type: string |
| 68 | format: date-time |
| 69 | |
| 70 | parameters: |
| 71 | PageCursor: |
| 72 | name: cursor |
| 73 | in: query |
| 74 | schema: |
| 75 | type: string |
| 76 | PageLimit: |
| 77 | name: limit |
| 78 | in: query |
| 79 | schema: |
| 80 | type: integer |
| 81 | minimum: 1 |
| 82 | maximum: 100 |
| 83 | default: 20 |
| 84 | |
| 85 | responses: |
| 86 | OrderListResponse: |
| 87 | description: Paginated list of orders |
| 88 | content: |
| 89 | application/json: |
| 90 | schema: |
| 91 | type: object |
| 92 | required: [data] |
| 93 | properties: |
| 94 | data: |
| 95 | type: array |
| 96 | items: |
| 97 | $ref: "#/components/schemas/Order" |
| 98 | next_cursor: |
| 99 | type: string |
| 100 | nullable: true |
| 101 | |
| 102 | securitySchemes: |
| 103 | bearerAuth: |
| 104 | type: http |
| 105 | scheme: bearer |
| 106 | bearerFormat: JWT |
| 107 | |
| 108 | security: |
| 109 | - bearerAuth: [] |
best practice
The most reliable way to keep documentation current is to generate the OpenAPI document from your server code. Frameworks like FastAPI, NestJS, Hono, and Express with libraries such as Zodios or tsoa can emit a spec from your route definitions and validation schemas.
| 1 | import { Hono } from "hono"; |
| 2 | import { z } from "zod"; |
| 3 | import { zValidator } from "@hono/zod-validator"; |
| 4 | import { openAPISpecs } from "@hono/zod-openapi"; |
| 5 | |
| 6 | const CreateOrderSchema = z.object({ |
| 7 | customerId: z.string().uuid(), |
| 8 | items: z.array(z.object({ |
| 9 | productId: z.string().uuid(), |
| 10 | quantity: z.number().int().min(1), |
| 11 | })).min(1), |
| 12 | }); |
| 13 | |
| 14 | const OrderSchema = z.object({ |
| 15 | id: z.string().uuid(), |
| 16 | customerId: z.string().uuid(), |
| 17 | status: z.enum(["pending", "paid", "shipped", "cancelled"]), |
| 18 | total: z.number().int(), |
| 19 | createdAt: z.string().datetime(), |
| 20 | }); |
| 21 | |
| 22 | const app = new Hono(); |
| 23 | |
| 24 | app.post("/orders", zValidator("json", CreateOrderSchema), async (c) => { |
| 25 | const body = c.req.valid("json"); |
| 26 | const order = await orderService.create(body); |
| 27 | return c.json(order, 201); |
| 28 | }); |
| 29 | |
| 30 | app.get("/openapi.json", async (c) => { |
| 31 | const spec = openAPISpecs(app, { |
| 32 | documentation: { |
| 33 | info: { title: "ForgeLearn API", version: "1.0.0" }, |
| 34 | servers: [{ url: "https://api.forgelearn.dev" }], |
| 35 | }, |
| 36 | }); |
| 37 | return c.json(spec); |
| 38 | }); |
info
OpenAPI documents can be rendered into interactive documentation by tools such as Swagger UI, Redoc, and Scalar. These UIs let developers explore endpoints, read schema definitions, and send test requests directly from the browser.
| Tool | Strengths | Best For |
|---|---|---|
| Swagger UI | Try-it-now, widely known | Internal dashboards |
| Redoc | Clean, readable layout | Public developer portals |
| Scalar | Modern UX, API client | Modern product docs |
note
OpenAPI enables typed client generation for many languages. Tools like OpenAPI Generator, Kiota, and Orval take a spec and produce HTTP clients with typed request and response models. Generated clients reduce boilerplate and catch contract drift at compile time.
| 1 | # Generate a TypeScript client with openapi-generator |
| 2 | npx @openapitools/openapi-generator-cli generate \ |
| 3 | -i openapi.json \ |
| 4 | -g typescript-fetch \ |
| 5 | -o ./src/generated/api \ |
| 6 | --additional-properties=supportsES6=true,npmName=@forgelearn/api-client |
| 7 | |
| 8 | # Or use Orval for React Query / SWR hooks |
| 9 | npx orval --config ./orval.config.js |
Treat generated code as a build artifact. Do not hand-edit generated files. Regenerate them in CI when the spec changes and commit the result so downstream projects can install a specific version.
warning
Contract testing verifies that the implementation matches the specification. Schemathesis, Dredd, and Prism can read an OpenAPI document and send requests to your server to check that responses conform to the documented schemas and status codes.
| 1 | # Validate server responses against openapi.json with schemathesis |
| 2 | st run openapi.json --base-url=http://localhost:3000 --checks=all |
| 3 | |
| 4 | # Start a mock server from the spec with Prism |
| 5 | npx @stoplight/prism-cli mock openapi.json |
| 6 | |
| 7 | # Use Prism as a validation proxy against a real server |
| 8 | npx @stoplight/prism-cli proxy openapi.json http://localhost:3000 --errors |
best practice
APIs evolve, and the OpenAPI document should evolve with them. Additive changes such as new optional fields or new endpoints do not require a new version. Breaking changes such as removing fields, changing types, or removing endpoints should be published under a new version.
Use the deprecated: true flag on endpoints and fields you plan to remove. Include sunset headers in responses to give consumers advance notice. Document migration paths in the description fields.
| 1 | /legacy/reports: |
| 2 | get: |
| 3 | operationId: listLegacyReports |
| 4 | deprecated: true |
| 5 | description: | |
| 6 | This endpoint is deprecated and will be removed on 2027-01-01. |
| 7 | Use `/v2/reports` instead. |
| 8 | responses: |
| 9 | "200": |
| 10 | description: Legacy report list |
| 11 | headers: |
| 12 | Sunset: |
| 13 | schema: |
| 14 | type: string |
| 15 | description: Date after which the endpoint will be removed. |
info