OpenAPI (Swagger) — Describing Endpoints, Schemas, and Generating Clients
Write a real OpenAPI 3.x specification describing an endpoint's parameters, request body, responses, and authentication; understand how that single YAML document drives interactive documentation (Swagger UI), request validation, and auto-generated client SDKs; and see how a PHP endpoint's actual behavior should be kept in sync with its spec.
Hand-written API documentation goes stale the moment a field changes and nobody updates the wiki page. OpenAPI solves this by making the documentation itself a machine-readable contract -- one YAML or JSON file describing every endpoint, its parameters, request and response shapes, and authentication requirements -- that tooling can then consume directly: Swagger UI renders it as interactive, always-in-sync documentation; validators check real requests/responses against it in CI; and generators produce fully-typed client libraries in dozens of languages from the same source. Instead of documentation describing the API from the outside, the spec becomes the API's actual source of truth.
Anatomy of an OpenAPI Document
An OpenAPI document has a handful of top-level sections. info carries the API's title, version, and description. servers lists base URLs (production, staging). paths is the heart of the document -- one entry per URL path, each with one entry per HTTP method, describing that operation's parameters, requestBody, responses, and which security scheme it requires. components holds reusable pieces -- schemas (data shapes referenced by $ref from multiple places), securitySchemes (how auth works), and reusable parameters or responses -- so a User schema defined once can be referenced from a dozen different endpoints instead of duplicated.
Every parameter and schema field declares a JSON Schema-compatible type (string, integer, boolean, array, object), and can carry constraints (minimum, maxLength, enum, format: email) plus a human-readable description and a concrete example value -- it's this combination of machine-checkable types and human-readable examples that lets the same document drive both automated validation and readable documentation.
• info -- title, version, description of the API as a whole. • paths -- one entry per URL, one operation per HTTP method, describing params/body/responses/auth. • components.schemas -- reusable data shapes referenced via $ref instead of duplicated everywhere. • components.securitySchemes -- declares HOW auth works (API key, bearer token, OAuth2) without repeating it on every operation.
From Spec to Documentation, Validation, and Generated Clients
Swagger UI and Redoc both take an OpenAPI document as input and render fully interactive documentation -- every endpoint, every parameter, a 'Try it out' button that fires a real request -- with zero hand-written HTML. Because the docs are generated FROM the spec rather than written separately, they cannot drift out of sync with it the way a hand-maintained wiki page inevitably does; updating the spec updates the docs automatically on next render.
The same document feeds request/response validation middleware (reject a request that's missing a required field or sends the wrong type, before it ever reaches application code) and, most powerfully, client SDK generators like openapi-generator or openapi-typescript-codegen, which read the spec and emit a fully-typed client library -- functions, request/response classes, even enum types -- in whatever target language a consumer needs (TypeScript, Python, Go, Java...), all generated automatically rather than hand-written and kept manually in sync.
This is why teams increasingly write the OpenAPI spec FIRST (design-first) rather than generating it from code comments after the fact (code-first): a spec written first can be reviewed and agreed upon by API consumers before a single line of implementation exists, catching shape disagreements while they're still cheap to fix.
• Swagger UI / Redoc render interactive docs directly from the spec -- can never drift out of sync because they ARE the spec, rendered. • Validation middleware checks real requests/responses against the spec's schemas automatically. • openapi-generator produces fully-typed client SDKs in many languages from one spec. • Design-first (write the spec, review it, then implement) catches shape disagreements before code exists.
A Complete OpenAPI 3.0 Operation Definition (YAML)
security: [bearerAuth: []] on the operation ties it to the securityScheme defined once in components -- no need to redescribe auth on every path. $ref: '#/components/schemas/Order' reuses the Order schema anywhere it's needed instead of duplicating its field list. Concrete example values make the rendered docs immediately readable, not just technically correct.
openapi: 3.0.3
info:
title: Orders API
version: 1.2.0
description: Manage customer orders.
servers:
- url: https://api.example.com/v1
paths:
/orders/{orderId}:
get:
operationId: getOrder
summary: Retrieve a single order by ID
security:
- bearerAuth: []
parameters:
- name: orderId
in: path
required: true
schema:
type: integer
minimum: 1
example: 42
- name: expand
in: query
required: false
description: Comma-separated related resources to embed
schema:
type: string
example: "customer,line_items"
responses:
'200':
description: Order found
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
example:
id: 42
status: shipped
total_cents: 4999
'404':
description: No order with that ID exists
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schema
…Code Examples
The implementation's response shape, required fields, and status codes deliberately mirror the spec exactly -- id, status (constrained to the same enum), and total_cents as an integer, plus the documented 404 error shape for a missing order. In a mature setup this alignment is enforced automatically by a contract test that loads the spec and validates real responses against its schemas in CI, rather than relying on developers remembering to keep code and spec manually in sync.
'status' => $order->status, // must be one of the enum values in the spec
<?php
declare(strict_types=1);
// GET /v1/orders/{orderId}?expand=customer,line_items
function getOrder(int $orderId, ?string $expand): void
{
$order = OrderRepository::find($orderId);
if ($order === null) {
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['message' => "No order with id {$orderId}"]);
return;
}
$payload = [
'id' => $order->id,
'status' => $order->status, // must be one of the enum values in the spec
'total_cents' => $order->totalCents,
];
if ($expand !== null && str_contains($expand, 'customer')) {
$payload['customer'] = ['id' => $order->customerId, 'name' => $order->customerName];
}
header('Content-Type: application/json');
http_response_code(200);
echo json_encode($payload);
}One spec, three generated outputs
Docs, validation, and client generation all derive from the same spec instead of being written three separate times.
openapi.yaml (single source of truth)
|
-------------------------------
| | |
Swagger UI Validation MW openapi-generator
(interactive | |
docs, "try it") reject bad TypeScript/Python/
requests Go client SDKsImportant Rules
Every parameter and response needs a schema with types and constraints -- 'it's a string' is not enough documentation to generate a useful client from.
Define reusable shapes once under components.schemas and reference them with $ref instead of duplicating field lists across endpoints.
Declare auth under components.securitySchemes once, then reference it per-operation with security: -- don't re-explain auth in every endpoint's description text.
Keep example values realistic and in sync with what the endpoint actually returns; misleading examples are worse than no examples.
Validate real request/response traffic against the spec in CI so the spec and the implementation cannot silently drift apart.
Design-first OpenAPI at scale
Large API platforms (Stripe, GitHub, many internal microservice fleets) write the OpenAPI spec before implementation, circulate it for review among the teams that will consume the API, and only then build against it -- often with the spec driving a mock server that consumer teams can integrate against in parallel with the real implementation being built. This turns API design disagreements into a cheap, pre-code conversation instead of an expensive post-launch breaking change.
Writing prose documentation by hand in a wiki, separate from the actual endpoint code, and updating it 'when there's time'.
Maintain a single OpenAPI spec as the source of truth, generate interactive docs from it, and validate it against real traffic in CI.
Hand-maintained docs inevitably drift from the real behavior of the code; a spec that tooling actively validates against real requests cannot silently go stale the same way.
Describing a field only as type: string with no enum, format, or example when its real values are a fixed, known set like order statuses.
Use enum: [pending, shipped, delivered, cancelled] plus a concrete example so generated clients get a proper type and docs readers see real values.
An under-specified schema produces a weaker generated client (a plain string instead of a typed enum/union) and less useful documentation than the endpoint's actual behavior supports.
What is the primary purpose of the components.schemas section in an OpenAPI document? A. To list which HTTP methods each endpoint supports B. To define reusable data shapes once, referenced via $ref from multiple operations C. To specify which servers host the API D. To store example API keys for testing
B. To define reusable data shapes once, referenced via $ref from multiple operations — components.schemas holds data shapes (like Order or Error) defined a single time and referenced with $ref wherever needed, avoiding duplication across dozens of endpoints.
Why do many teams prefer writing an OpenAPI spec 'design-first' rather than generating it from code after implementation? A. Design-first specs run faster in production B. It lets API consumers review and agree on the shape before any implementation code is written, catching disagreements early C. Code-first generation is not supported by any tooling D. Design-first specs don't need a version field
B. It lets API consumers review and agree on the shape before any implementation code is written, catching disagreements early — A spec written and reviewed before implementation lets consuming teams flag shape problems while they are still cheap to change, rather than after a breaking implementation already exists.
Extend the YAML spec above with a POST /orders operation: it accepts a JSON request body with required fields customer_id (integer) and line_items (array of objects, each with sku string and quantity integer >= 1), requires bearerAuth, and returns 201 with the created Order on success or 422 with the Error schema on validation failure.
post:\n operationId: createOrder\n security: [{bearerAuth: []}]\n requestBody:\n required: true\n content:\n application/json:\n schema:\n type: object\n required: [customer_id, line_items]\n properties:\n customer_id: {type: integer}\n line_items:\n type: array\n items:\n type: object\n properties:\n sku: {type: string}\n quantity: {type: integer, minimum: 1}\n responses:\n '201': {description: Created, content: {application/json: {schema: {$ref: '#/components/schemas/Order'}}}}\n '422': {description: Validation failed, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
Use requestBody.content.application/json.schema for the input shape, and add a new response entry for '201' and '422' alongside the existing '200'/'404' pattern.