Skip to main content

OpenAPI Specification: Documenting Your API

You've designed TaskFlow's URLs, methods, payloads, headers, and versioning strategy. If someone asked you to explain the API right now, you could — you have all the information in your head.

But your API isn't just for you. It's for the iOS developer on your team who needs to know what fields a task response contains. It's for the third-party developer who wants to integrate TaskFlow into their product. It's for the QA engineer who wants to validate that every endpoint behaves as documented. It's for the code generator that could produce a typed client library automatically if only there were a machine-readable description.

That's what OpenAPI is. It's a standard format for describing REST APIs in a way that both humans and machines can read. Write an OpenAPI spec for TaskFlow and you get: interactive documentation your team can explore in a browser, mock servers that return realistic responses before your backend is built, automated request validation, and client SDKs in any language — all from a single source of truth.

This article walks through the complete OpenAPI spec for TaskFlow, section by section. By the end, you'll understand every part of the format and have a working spec you can use immediately.


Quick Reference

OpenAPI document structure:

openapi: 3.1.0
info: # API metadata
paths: # Endpoints and operations
components: # Reusable schemas, parameters, responses
security: # Global security requirements
tags: # Grouping for documentation

A minimal endpoint definition:

paths:
/v1/tasks:
get:
summary: List tasks
parameters:
- $ref: "#/components/parameters/LimitParam"
responses:
"200":
description: A list of tasks
content:
application/json:
schema:
$ref: "#/components/schemas/TaskListResponse"

$ref — the DRY mechanism:

$ref: '#/components/schemas/Task'         # Local reference
$ref: './schemas/task.yaml' # External file reference

Key tools:

Gotchas:

  • ⚠️ OpenAPI 3.0 and 3.1 are not identical — 3.1 aligns with JSON Schema; 3.0 does not
  • ⚠️ $ref replaces its entire sibling object — you can't combine $ref with other properties in OpenAPI 3.0 (3.1 fixes this)
  • ⚠️ The spec describes your API as it should behave — it's not automatically enforced unless you add a validation middleware

See also:


Version Information

Relevant specifications:

Note: This article uses OpenAPI 3.1.0, the current version as of 2025. If you're working with an existing spec at 3.0.x, the structure is nearly identical — key differences are called out inline.

Tools referenced:

  • Swagger UI: 5.x
  • Redoc: 2.x
  • openapi-typescript: ^7.0

Last verified: June 2025


What You Need to Know First

Required reading (in order):

  1. REST APIs: What They Are and How They Work — OpenAPI describes REST APIs; the concepts map directly
  2. Resource Design: URLs, Nouns, and Hierarchies — TaskFlow's URL structure is what we're documenting
  3. HTTP Methods and Status Codes: The Full Picture — every operation in the spec needs correct methods and status codes
  4. Request & Response Design: Payloads, Headers, and Conventions — the payload shapes we designed are what we're now formalizing

Helpful background:

  • Basic YAML syntax — indentation, key-value pairs, lists (we'll explain as we go, but familiarity helps)
  • JSON Schema basics are helpful but not required

What We'll Cover in This Article

By the end of this guide, you'll understand:

  • What OpenAPI is and what it's used for — documentation, mocking, validation, code generation
  • The top-level structure of an OpenAPI 3.1 document
  • How to define paths, operations, parameters, and request/response bodies
  • How to use $ref and components to keep specs DRY
  • How to document TaskFlow's complete API surface in a working spec
  • Which tools to use to render, validate, and generate code from your spec

What We'll Explain Along the Way

Don't worry if you're unfamiliar with these — we'll define them as we encounter them:

  • YAML syntax (briefly)
  • JSON Schema keywords (type, properties, required, enum, nullable)
  • $ref and what "resolving a reference" means
  • The difference between OpenAPI 3.0 and 3.1

What OpenAPI Is

OpenAPI is a specification for describing REST APIs in a structured, machine-readable format. A spec is a YAML (or JSON) file that describes:

  • What endpoints exist
  • What HTTP methods each endpoint supports
  • What parameters each operation accepts
  • What the request body looks like
  • What every possible response looks like
  • What authentication is required

It's a contract between your API and its consumers — written down in a format that tools can read and act on.

What You Get from a Spec

Once you have an OpenAPI spec, you unlock a toolchain:

Interactive documentation: Tools like Swagger UI and Redoc render your spec as navigable, interactive documentation. Developers can browse endpoints, see example requests and responses, and even make live API calls — all from a browser, without reading a word of code.

Mock servers: Tools like Prism read your spec and spin up a server that returns realistic example responses — before your backend exists. Frontend teams can build against the spec while the backend team is still writing the implementation.

Request validation: Middleware like express-openapi-validator validates every incoming request against your spec automatically. Malformed requests are rejected before they reach your handlers — with error messages that match your error format.

Client SDK generation: Tools like openapi-generator read your spec and produce typed client libraries in dozens of languages. Your iOS team gets a Swift SDK. Your web team gets TypeScript types. Both generated from the same spec.

Contract testing: Your spec is a contract. Test frameworks can verify that your actual server responses match what the spec promises — catching regressions before they reach clients.

All of this from a single YAML file.

OpenAPI vs Swagger

"Swagger" is the original name of the specification. In 2016, it was donated to the OpenAPI Initiative and renamed to "OpenAPI." Tools still use the Swagger name (Swagger UI, Swagger Editor) — these are tools for working with OpenAPI specs, not a different format. The spec itself is "OpenAPI."


A Minimal YAML Primer

OpenAPI specs are written in YAML. If you're not familiar, here's what you need to know in 60 seconds:

# Key-value pairs — indentation defines structure
name: TaskFlow API
version: 1.0.0

# Nested objects — indent with 2 spaces
info:
title: TaskFlow API
version: 1.0.0

# Lists — prefix items with "- "
tags:
- name: Tasks
- name: Projects

# Multi-line strings
description: |
This is a multi-line
description string.

# References to elsewhere in the document
schema:
$ref: "#/components/schemas/Task"

YAML is whitespace-sensitive. Indentation must be consistent — use 2 spaces throughout (not tabs). That's essentially the entire syntax you need for an OpenAPI spec.


Top-Level OpenAPI Structure

Every OpenAPI document has the same skeleton:

openapi: 3.1.0

info:
title: TaskFlow API
description: Task management API for the TaskFlow product.
version: 1.0.0
contact:
name: TaskFlow API Support
email: api@taskflow.com
url: https://docs.taskflow.com

servers:
- url: https://api.taskflow.com
description: Production
- url: https://api.staging.taskflow.com
description: Staging
- url: http://localhost:3000
description: Local development

tags:
- name: Tasks
description: Create, read, update, and delete tasks
- name: Projects
description: Manage project containers for tasks
- name: Users
description: User account management
- name: Comments
description: Task comments

paths:
# All endpoints defined here

components:
schemas:
# Reusable schema definitions
parameters:
# Reusable parameter definitions
responses:
# Reusable response definitions
securitySchemes:
# Authentication schemes

security:
- BearerAuth: [] # Global security — applies to all operations

Let's go through each section.

openapi — the version of the OpenAPI specification this document uses. Always 3.1.0 for new specs.

info — metadata about the API. title and version are required. description, contact, and license are optional but valuable.

servers — the base URLs where the API is hosted. Swagger UI uses these for live API calls. List all environments (production, staging, local) so developers can switch without editing the spec.

tags — grouping labels. Operations reference tags by name; documentation tools use them to organize endpoints into sections. Define all tags at the top level so they appear in the order you want.

paths — the heart of the spec. Every endpoint is defined here.

components — a library of reusable definitions. Anything defined here can be referenced with $ref anywhere else in the document. This is how you keep the spec DRY.

security — global security requirements. Applying security here means every operation requires authentication unless it explicitly overrides with security: [].


Defining Paths and Operations

The paths section maps URL paths to the operations they support. Let's build the task endpoints.

A Simple GET Endpoint

paths:
/v1/tasks:
get:
operationId: listTasks
summary: List tasks
description: |
Returns a paginated list of tasks. Results are sorted by creation date
(newest first) by default. Use query parameters to filter and sort.
tags:
- Tasks
parameters:
- name: status
in: query
description: Filter tasks by status
required: false
schema:
type: string
enum: [open, in_progress, complete, archived]
- name: assigneeId
in: query
description: Filter tasks by assignee user ID
required: false
schema:
type: string
example: user_42
- name: limit
in: query
description: Maximum number of results to return (max 100)
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: cursor
in: query
description: Pagination cursor from a previous response
required: false
schema:
type: string
responses:
"200":
description: A paginated list of tasks
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Task"
meta:
$ref: "#/components/schemas/PaginationMeta"
example:
data:
- id: task_99
title: Review pull request
assigneeId: user_42
projectId: proj_5
status: open
dueDate: "2025-08-01"
createdAt: "2025-06-15T10:30:00Z"
updatedAt: "2025-06-15T10:30:00Z"
meta:
cursor: dXNlcjox
hasMore: true
limit: 20
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"

Let's unpack the key parts:

operationId — a unique identifier for this operation. Used by code generators to name functions: listTasks(), createTask(), etc. Must be unique across the entire spec. Use camelCase.

parameters — an array of parameter definitions. Each parameter has:

  • name — the parameter name
  • in — where it appears: query, path, header, or cookie
  • required — whether it's mandatory (defaults to false)
  • schema — the JSON Schema definition of the parameter's type and constraints

responses — a map of status codes to response definitions. Every possible status code your operation might return should be listed. $ref pulls in shared response definitions from components.

A POST Endpoint with a Request Body

/v1/tasks:
# get: ... (defined above)
post:
operationId: createTask
summary: Create a task
description: Creates a new task. Returns the created task with its server-assigned ID.
tags:
- Tasks
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateTaskRequest"
example:
title: Review pull request #204
assigneeId: user_42
projectId: proj_5
dueDate: "2025-08-01"
responses:
"201":
description: Task created successfully
headers:
Location:
description: URL of the newly created task
schema:
type: string
example: /v1/tasks/task_99
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Task"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"422":
$ref: "#/components/responses/ValidationError"

requestBody — defines what the client sends. required: true means the body is mandatory. content maps MIME types to schema definitions. Most APIs only need application/json.

headers in a response — documents response headers. Here we document the Location header that points to the newly created task.

Path Parameters

/v1/tasks/{taskId}:
get:
operationId: getTask
summary: Get a task
tags:
- Tasks
parameters:
- name: taskId
in: path
required: true # Path parameters are always required
description: The unique identifier of the task
schema:
type: string
example: task_99
responses:
"200":
description: The requested task
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Task"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"

patch:
operationId: updateTask
summary: Update a task
description: |
Partially updates a task. Only fields included in the request body are
modified — omitted fields are left unchanged.
tags:
- Tasks
parameters:
- name: taskId
in: path
required: true
schema:
type: string
example: task_99
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateTaskRequest"
responses:
"200":
description: Task updated successfully
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Task"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"422":
$ref: "#/components/responses/ValidationError"

delete:
operationId: deleteTask
summary: Delete a task
description: Permanently deletes a task and all its comments.
tags:
- Tasks
parameters:
- name: taskId
in: path
required: true
schema:
type: string
example: task_99
responses:
"204":
description: Task deleted successfully
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"

Notice how get, patch, and delete all live under the same path (/v1/tasks/{taskId}), indented as siblings. Each is a separate operation on the same resource URL.


Components: Keeping the Spec DRY

Without components, every operation would repeat the same schema definitions inline. A 20-endpoint API would have the same Task object defined in 20 places — and every time the Task shape changes, you'd update it 20 times.

components is the spec's library. Define things once, reference them everywhere with $ref.

Schemas

components:
schemas:
# ─────────────────────────────────────────────
# Core resource: Task
# ─────────────────────────────────────────────
Task:
type: object
description: A single unit of work in TaskFlow
required:
- id
- title
- status
- createdAt
- updatedAt
properties:
id:
type: string
description: Unique identifier for the task
example: task_99
readOnly: true # Server-assigned; not accepted in requests
title:
type: string
description: Short description of the task
example: Review pull request #204
minLength: 1
maxLength: 500
assigneeId:
type: string
nullable: true
description: ID of the user assigned to this task
example: user_42
projectId:
type: string
nullable: true
description: ID of the project this task belongs to
example: proj_5
status:
type: string
description: Current status of the task
enum: [open, in_progress, complete, archived]
default: open
dueDate:
type: string
format: date
nullable: true
description: Due date in ISO 8601 date format (YYYY-MM-DD)
example: "2025-08-01"
createdAt:
type: string
format: date-time
description: Timestamp when the task was created (ISO 8601)
example: "2025-06-15T10:30:00Z"
readOnly: true
updatedAt:
type: string
format: date-time
description: Timestamp when the task was last updated (ISO 8601)
example: "2025-06-15T10:30:00Z"
readOnly: true

# ─────────────────────────────────────────────
# Request body: Create task
# ─────────────────────────────────────────────
CreateTaskRequest:
type: object
description: Fields required to create a new task
required:
- title
properties:
title:
type: string
description: Short description of the task
example: Review pull request #204
minLength: 1
maxLength: 500
assigneeId:
type: string
nullable: true
description: ID of the user to assign this task to
example: user_42
projectId:
type: string
nullable: true
description: ID of the project to add this task to
example: proj_5
dueDate:
type: string
format: date
nullable: true
description: Due date in ISO 8601 date format (YYYY-MM-DD)
example: "2025-08-01"

# ─────────────────────────────────────────────
# Request body: Update task (all fields optional)
# ─────────────────────────────────────────────
UpdateTaskRequest:
type: object
description: Fields to update on an existing task. All fields are optional.
properties:
title:
type: string
minLength: 1
maxLength: 500
example: Updated task title
assigneeId:
type: string
nullable: true
example: user_42
status:
type: string
enum: [open, in_progress, complete, archived]
example: complete
dueDate:
type: string
format: date
nullable: true
example: "2025-09-01"

# ─────────────────────────────────────────────
# Pagination metadata
# ─────────────────────────────────────────────
PaginationMeta:
type: object
required:
- cursor
- hasMore
- limit
properties:
cursor:
type: string
nullable: true
description: Opaque cursor for the next page. Null when no more pages exist.
example: dXNlcjox
hasMore:
type: boolean
description: Whether more results exist beyond this page
example: true
limit:
type: integer
description: Number of results per page used for this response
example: 20

# ─────────────────────────────────────────────
# Error response
# ─────────────────────────────────────────────
ErrorResponse:
type: object
required:
- error
properties:
error:
type: object
required:
- code
- message
- details
properties:
code:
type: string
description: Machine-readable error code
example: validation_error
message:
type: string
description: Human-readable error description
example: The request contains invalid fields.
details:
type: array
items:
type: object
properties:
field:
type: string
description: The field that caused the error
example: dueDate
message:
type: string
description: What went wrong with this field
example: Must be a future date.
value:
description: The value that was rejected
example: "2020-01-01"

# ─────────────────────────────────────────────
# Comment resource
# ─────────────────────────────────────────────
Comment:
type: object
required:
- id
- taskId
- authorId
- body
- createdAt
- updatedAt
properties:
id:
type: string
readOnly: true
example: comment_7
taskId:
type: string
readOnly: true
example: task_99
authorId:
type: string
readOnly: true
example: user_42
body:
type: string
description: The comment text
example: Looks good to me — ready to merge.
minLength: 1
maxLength: 10000
createdAt:
type: string
format: date-time
readOnly: true
example: "2025-06-15T11:00:00Z"
updatedAt:
type: string
format: date-time
readOnly: true
example: "2025-06-15T11:00:00Z"

Key JSON Schema keywords used here:

  • type — data type: string, integer, number, boolean, array, object
  • required — array of property names that must be present
  • properties — defines the shape of an object
  • enum — restricts a string to a set of allowed values
  • nullable: true — allows the field to be null (OpenAPI 3.0 extension; in 3.1, use type: [string, 'null'])
  • readOnly: true — field appears in responses but is ignored in requests
  • format — semantic hint: date, date-time, email, uuid
  • minLength / maxLength — string length constraints
  • minimum / maximum — numeric constraints
  • example — example value shown in documentation

Reusable Responses

Instead of writing the same 401 Unauthorized response body in every operation, define it once:

components:
responses:
Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error:
code: authentication_required
message: This endpoint requires authentication. Provide a valid Bearer token.
details: []

Forbidden:
description: Insufficient permissions
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error:
code: insufficient_permissions
message: You do not have permission to perform this action.
details: []

NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error:
code: not_found
message: The requested resource does not exist.
details: []

BadRequest:
description: Malformed request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error:
code: invalid_json
message: The request body is not valid JSON.
details: []

ValidationError:
description: Request body failed validation
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error:
code: validation_error
message: The request contains invalid fields.
details:
- field: dueDate
message: Must be a future date.
value: "2020-01-01"

RateLimited:
description: Too many requests
headers:
Retry-After:
description: Number of seconds to wait before retrying
schema:
type: integer
example: 30
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error:
code: rate_limited
message: Too many requests. Retry after 30 seconds.
details: []

Reusable Parameters

Common parameters — pagination cursors, limit values — appear on dozens of endpoints. Define them once:

components:
parameters:
TaskIdParam:
name: taskId
in: path
required: true
description: Unique identifier of the task
schema:
type: string
example: task_99

LimitParam:
name: limit
in: query
required: false
description: Maximum number of results (1–100, default 20)
schema:
type: integer
minimum: 1
maximum: 100
default: 20

CursorParam:
name: cursor
in: query
required: false
description: Pagination cursor from a previous response
schema:
type: string
example: dXNlcjox

Now any operation that needs pagination just references them:

parameters:
- $ref: "#/components/parameters/LimitParam"
- $ref: "#/components/parameters/CursorParam"

Security Schemes

components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
All endpoints require a Bearer token in the Authorization header.
Obtain a token via POST /v1/auth/login.

Example: Authorization: Bearer eyJhbGci...

Security in the Spec

Apply security globally (all endpoints require auth) and override per-operation where needed:

# Top-level — applies to all operations
security:
- BearerAuth: []

paths:
/v1/tasks:
get:
# Inherits global BearerAuth — no security override needed
...

/v1/health:
get:
operationId: healthCheck
summary: Health check
security: [] # Override: this endpoint requires NO auth
responses:
"200":
description: API is healthy

The Complete TaskFlow Spec Skeleton

Here's the full skeleton with all resources stubbed in — ready to expand with the full operation definitions:

openapi: 3.1.0

info:
title: TaskFlow API
description: |
Task management API for the TaskFlow product.

## Authentication
All endpoints require a Bearer token unless marked as public.
Obtain a token via `POST /v1/auth/login`.

## Versioning
This is version 1 of the TaskFlow API. The API version is indicated
by the `/v1/` prefix on all endpoint paths.

## Pagination
Collection endpoints use cursor-based pagination. Pass the `cursor`
value from `meta.cursor` as a query parameter to fetch the next page.
A null cursor means no more pages exist.

## Error Format
All error responses follow a consistent format:
```json
{
"error": {
"code": "machine_readable_code",
"message": "Human-readable description.",
"details": []
}
}
```
version: 1.0.0
contact:
name: TaskFlow API Support
email: api@taskflow.com
url: https://docs.taskflow.com

servers:
- url: https://api.taskflow.com
description: Production
- url: https://api.staging.taskflow.com
description: Staging
- url: http://localhost:3000
description: Local development

tags:
- name: Tasks
description: Create, read, update, and delete tasks
- name: Projects
description: Manage project containers for tasks
- name: Users
description: User account and profile management
- name: Comments
description: Comments on tasks

security:
- BearerAuth: []

paths:
# ── Tasks ─────────────────────────────────────
/v1/tasks:
get:
operationId: listTasks
summary: List tasks
tags: [Tasks]
parameters:
- name: status
in: query
schema:
type: string
enum: [open, in_progress, complete, archived]
- name: assigneeId
in: query
schema:
type: string
- name: projectId
in: query
schema:
type: string
- name: sort
in: query
schema:
type: string
default: "-createdAt"
example: "-dueDate"
- $ref: "#/components/parameters/LimitParam"
- $ref: "#/components/parameters/CursorParam"
responses:
"200":
description: Paginated list of tasks
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Task"
meta:
$ref: "#/components/schemas/PaginationMeta"
"401":
$ref: "#/components/responses/Unauthorized"

post:
operationId: createTask
summary: Create a task
tags: [Tasks]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateTaskRequest"
responses:
"201":
description: Task created
headers:
Location:
schema:
type: string
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Task"
"401":
$ref: "#/components/responses/Unauthorized"
"422":
$ref: "#/components/responses/ValidationError"

/v1/tasks/{taskId}:
get:
operationId: getTask
summary: Get a task
tags: [Tasks]
parameters:
- $ref: "#/components/parameters/TaskIdParam"
responses:
"200":
description: The requested task
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Task"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"

patch:
operationId: updateTask
summary: Update a task
tags: [Tasks]
parameters:
- $ref: "#/components/parameters/TaskIdParam"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateTaskRequest"
responses:
"200":
description: Task updated
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Task"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"422":
$ref: "#/components/responses/ValidationError"

delete:
operationId: deleteTask
summary: Delete a task
tags: [Tasks]
parameters:
- $ref: "#/components/parameters/TaskIdParam"
responses:
"204":
description: Task deleted
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"

# ── Comments ──────────────────────────────────
/v1/tasks/{taskId}/comments:
get:
operationId: listComments
summary: List comments on a task
tags: [Comments]
parameters:
- $ref: "#/components/parameters/TaskIdParam"
- $ref: "#/components/parameters/LimitParam"
- $ref: "#/components/parameters/CursorParam"
responses:
"200":
description: Paginated list of comments
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Comment"
meta:
$ref: "#/components/schemas/PaginationMeta"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"

post:
operationId: createComment
summary: Add a comment to a task
tags: [Comments]
parameters:
- $ref: "#/components/parameters/TaskIdParam"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [body]
properties:
body:
type: string
minLength: 1
maxLength: 10000
example: Looks good to me — ready to merge.
responses:
"201":
description: Comment created
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Comment"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"422":
$ref: "#/components/responses/ValidationError"

# ── Projects, Users — follow same pattern ─────
# (abbreviated for space — structure is identical)

components:
schemas:
Task:
# ... (defined in full above)
CreateTaskRequest:
# ...
UpdateTaskRequest:
# ...
Comment:
# ...
PaginationMeta:
# ...
ErrorResponse:
# ...

parameters:
TaskIdParam:
# ...
LimitParam:
# ...
CursorParam:
# ...

responses:
Unauthorized:
# ...
Forbidden:
# ...
NotFound:
# ...
BadRequest:
# ...
ValidationError:
# ...
RateLimited:
# ...

securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT

Tools for Working with OpenAPI

Swagger UI

Swagger UI renders your spec as interactive HTML documentation. Developers can browse endpoints, expand request/response schemas, and make live API calls in the browser.

// Adding Swagger UI to an Express server
import swaggerUi from "swagger-ui-express";
import YAML from "yamljs";

const swaggerDocument = YAML.load("./openapi.yaml");

app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
// Documentation available at http://localhost:3000/docs

Redoc

Redoc produces a clean, three-panel documentation layout — navigation on the left, content in the center, code samples on the right. Many teams prefer it for public-facing docs.

<!-- Standalone HTML page using Redoc CDN -->
<!DOCTYPE html>
<html>
<head>
<title>TaskFlow API Documentation</title>
</head>
<body>
<redoc spec-url="https://api.taskflow.com/openapi.yaml"></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
</body>
</html>

Generating TypeScript Types

openapi-typescript generates TypeScript types directly from your OpenAPI spec:

npx openapi-typescript ./openapi.yaml -o ./src/types/api.ts

The generated file contains types for every schema in your spec:

// Auto-generated from openapi.yaml — do not edit manually
export interface Task {
id: string;
title: string;
assigneeId: string | null;
projectId: string | null;
status: "open" | "in_progress" | "complete" | "archived";
dueDate: string | null;
createdAt: string;
updatedAt: string;
}

export interface CreateTaskRequest {
title: string;
assigneeId?: string | null;
projectId?: string | null;
dueDate?: string | null;
}

Your frontend team gets full type safety. Your spec and your types are always in sync — because the types are generated from the spec.

Request Validation Middleware

express-openapi-validator validates every request and response against your spec automatically:

import OpenApiValidator from "express-openapi-validator";

app.use(
OpenApiValidator.middleware({
apiSpec: "./openapi.yaml",
validateRequests: true, // Validate incoming requests
validateResponses: true, // Validate outgoing responses (useful in dev)
}),
);

// Invalid requests are now rejected automatically before reaching handlers
// with a 400 response matching your error format

With this in place, your spec is enforced — not just documented.


Common Misconceptions

❌ Misconception: The spec automatically enforces your API's behavior

Reality: An OpenAPI spec is a description, not enforcement. Your server doesn't automatically validate requests or format responses to match the spec — unless you add validation middleware. A spec without validation is documentation. A spec with validation middleware is a contract.

Fix: Use express-openapi-validator (or an equivalent for your framework) to enforce the spec at runtime. Run response validation in development to catch spec drift early.

❌ Misconception: You should write the spec after the code

Reality: Writing the spec first — spec-first development — is significantly more effective. When you write the spec first, you design the API on paper before committing to an implementation. Changing a field name in YAML costs nothing. Changing a field name in a live API requires a migration.

Spec-first workflow:

  1. Write the spec collaboratively with your team
  2. Generate mock server from the spec (Prism)
  3. Frontend builds against the mock
  4. Backend implements to match the spec
  5. Validate implementation against spec with middleware

❌ Misconception: OpenAPI 3.0 and 3.1 are the same

Reality: OpenAPI 3.1 fully aligns with JSON Schema 2020-12. OpenAPI 3.0 uses a modified JSON Schema dialect with important differences:

  • nullable: true (3.0) vs type: ["string", "null"] (3.1)
  • $ref siblings are ignored in 3.0; they work in 3.1
  • exclusiveMinimum/exclusiveMaximum have different semantics

Use 3.1 for new specs. If you're on 3.0, check your tooling — not all tools support 3.1 fully yet as of 2025.

❌ Misconception: Every possible response must be documented

Reality: Document every response your API intentionally produces. Don't document responses your framework automatically generates (like a generic 500 from an uncaught exception) — those are implementation details, not part of your API contract.

At minimum, document: all 2xx responses, all 4xx responses your validation logic produces, and 500 as a general server error. Use default for catch-all error handling:

responses:
"200":
description: Success
"401":
$ref: "#/components/responses/Unauthorized"
default:
description: Unexpected server error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"

Troubleshooting Common Issues

Problem: Swagger UI shows no examples in response schemas

Symptoms: The documentation shows the schema structure but no example values.

Fix: Add example fields to your schema properties, or add an examples block at the operation level:

responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/Task"
example: # ← Operation-level example overrides schema examples
data:
id: task_99
title: Review pull request
status: open

Problem: $ref is resolving to the wrong schema

Symptoms: A reference produces unexpected schema output, or a validator reports a missing $ref target.

Diagnostic steps:

# Use the Redocly CLI to validate your spec
npx @redocly/cli lint openapi.yaml

# Check all $ref paths are correct
npx @redocly/cli bundle openapi.yaml --output bundled.yaml
# If bundling fails, a $ref path is wrong

Common causes:

  • Typo in the $ref path — YAML is case-sensitive
  • Component name doesn't match the reference path (e.g., defined as TaskResponse but referenced as TaskRes)
  • Indentation error causing the $ref to be nested incorrectly

Problem: Generated TypeScript types are too broad (any or unknown)

Symptoms: openapi-typescript produces types with any for fields you expected to be typed.

Fix: Ensure every schema property has an explicit type. OpenAPI schemas without type are treated as accepting any value:

# ❌ Missing type — generates `any`
properties:
value:
description: The field value

# ✅ Explicit type — generates correct TypeScript type
properties:
value:
type: string
description: The field value

Check Your Understanding

Quick Quiz

  1. What is the purpose of operationId in an OpenAPI spec?

    Show Answer

    operationId is a unique string identifier for each operation in the spec. It must be unique across the entire document.

    Code generation tools use it to name functions in generated client SDKs — operationId: listTasks produces a listTasks() function. Documentation tools use it as an anchor for deep-linking to specific operations.

    Use camelCase and make it descriptive: listTasks, createTask, getTask, updateTask, deleteTask.

  2. What does readOnly: true on a schema property communicate?

    Show Answer

    It means the field appears in responses but should be ignored (or omitted) in requests. Server-assigned fields like id, createdAt, and updatedAt are readOnly — clients don't send them, the server generates them.

    Validation middleware will reject request bodies that include readOnly fields (or silently strip them, depending on configuration).

  3. You have the same taskId path parameter defined on 15 different endpoints. How do you avoid repeating it 15 times?

    Show Answer

    Define it once in components/parameters:

    components:
    parameters:
    TaskIdParam:
    name: taskId
    in: path
    required: true
    schema:
    type: string
    example: task_99

    Then reference it in every operation:

    parameters:
    - $ref: "#/components/parameters/TaskIdParam"
  4. What's the difference between adding an example on a schema property vs. adding an example at the operation's response level?

    Show Answer

    A schema-level example lives inside the components/schemas definition and travels with the schema wherever it's referenced. It shows an example value for that field in isolation.

    An operation-level example lives in the responses block of a specific operation and shows the complete response body for that exact endpoint. It overrides schema-level examples in documentation renderers.

    Use schema-level examples for field-by-field documentation. Use operation-level examples for complete, realistic end-to-end request/response illustrations.

Hands-On Exercise

Challenge: Write the full OpenAPI path definition for the "Add a comment to a task" operation (POST /v1/tasks/{taskId}/comments), including:

  • Operation metadata (operationId, summary, tags)
  • Path parameter using $ref
  • Request body schema (inline, not $ref)
  • All expected responses — 201, 401, 404, 422 — using $ref where possible
  • A realistic operation-level example for the 201 response
Show Answer
/v1/tasks/{taskId}/comments:
post:
operationId: createComment
summary: Add a comment to a task
description: |
Adds a new comment to the specified task.
Returns the created comment with its server-assigned ID.
tags:
- Comments
parameters:
- $ref: "#/components/parameters/TaskIdParam"
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- body
properties:
body:
type: string
description: The comment text
minLength: 1
maxLength: 10000
example: Looks good to me — ready to merge.
example:
body: Looks good to me — ready to merge.
responses:
"201":
description: Comment created successfully
headers:
Location:
description: URL of the newly created comment
schema:
type: string
example: /v1/tasks/task_99/comments/comment_7
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Comment"
example:
data:
id: comment_7
taskId: task_99
authorId: user_42
body: Looks good to me — ready to merge.
createdAt: "2025-06-15T11:00:00Z"
updatedAt: "2025-06-15T11:00:00Z"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"422":
$ref: "#/components/responses/ValidationError"

Summary: Key Takeaways

  • OpenAPI is a machine-readable contract for your REST API — a YAML document that describes every endpoint, every parameter, every request body, and every response shape.

  • One spec unlocks an entire toolchain: interactive documentation (Swagger UI, Redoc), mock servers (Prism), request validation middleware, TypeScript type generation, and client SDK generation — all from a single source of truth.

  • The spec has five top-level sections: info (metadata), servers (base URLs), paths (endpoint definitions), components (reusable definitions), and security (auth requirements).

  • $ref keeps the spec DRY. Define schemas, parameters, and responses once in components, reference them everywhere with $ref: '#/components/schemas/Task'. Changes to a component propagate automatically.

  • Spec-first development beats spec-after. Writing the spec before the implementation lets you validate API design decisions on paper — when changes are cheap. Frontend teams can build against a mock server from day one.

  • The spec describes; middleware enforces. A spec alone is documentation. Add express-openapi-validator (or equivalent) to make it a runtime contract that rejects non-conforming requests automatically.


What's Next?

You now have a complete, machine-readable description of TaskFlow's API — structure, payloads, schemas, and error formats all formalized.

The natural next step is Authentication and Authorization: API Security Patterns (coming soon) — where we add the securitySchemes section fully, design TaskFlow's auth flow, and work through the four OAuth 2.0 grant types. Security isn't an afterthought you bolt on at the end — it's a design concern that changes how you structure several endpoints, and the spec you've just written is the foundation you'll build it on.


References

  • OpenAPI Specification 3.1.0 — The official OpenAPI specification. Primary reference for all YAML structure, keywords, and field definitions used throughout this article.
  • JSON Schema 2020-12 — The JSON Schema specification that OpenAPI 3.1 fully adopts. Reference for type, properties, required, enum, format, and constraint keywords.
  • Swagger UI — Interactive documentation renderer for OpenAPI specs. Referenced in the tools section and the Express integration example.
  • Redoc — Three-panel documentation renderer. Referenced in the tools section and the standalone HTML example.
  • openapi-typescript — TypeScript type generator for OpenAPI specs. Referenced for the type generation workflow and generated type examples.
  • express-openapi-validator — Request and response validation middleware for Express. Referenced for the spec enforcement pattern.
  • Redocly CLI — OpenAPI linting and bundling tool. Referenced in the troubleshooting section for spec validation.
  • Stoplight Studio — Visual OpenAPI editor. Mentioned in the Quick Reference as a design tool.