OpenAPI Documentation And Swagger Tooling
OpenAPI is a standard, language-agnostic way to describe HTTP APIs. It documents paths, methods, parameters, request bodies, response bodies, status codes, authentication, examples, and reusable schemas. The official latest OpenAPI Specification reviewed for this lesson is 3.2.0, dated 19 September 2025.
Good OpenAPI documentation is more than a browsable reference page. It is a contract that humans and tools can use to understand, call, validate, test, generate clients for, and review an API. It is valuable only when it matches the deployed behavior.
Swagger is historically connected to OpenAPI and is still used in tooling names such as Swagger UI and Swagger Editor. In modern PHP projects, people often say "Swagger docs" when they mean an OpenAPI document rendered by Swagger tooling.
What An Operation Describes
An OpenAPI operation describes one HTTP method on one path. A useful operation gives clients enough information to make the request and handle expected outcomes.
<?php
declare(strict_types=1);
$operation = [
'method' => 'GET',
'path' => '/products/{id}',
'operationId' => 'getProduct',
'parameters' => [
['name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'string']],
],
'responses' => [
'200' => ['description' => 'Product found'],
'404' => ['description' => 'Product not found'],
],
];
echo $operation['method'] . ' ' . $operation['path'] . PHP_EOL;
echo implode(', ', array_keys($operation['responses'])) . PHP_EOL;
// Prints:
// GET /products/{id}
// 200, 404
A complete operation usually documents path parameters, query parameters, headers, request body, response bodies by status code, security requirements, examples, and error responses. A generated client can only be as good as this detail.
A Minimal Document Shape
Real OpenAPI is usually written as YAML or JSON. PHP arrays are used here only to keep examples executable.
<?php
declare(strict_types=1);
$document = [
'openapi' => '3.2.0',
'info' => ['title' => 'Products API', 'version' => '1.0.0'],
'servers' => [['url' => 'https://api.example.test']],
'paths' => [
'/products/{id}' => [
'get' => [
'operationId' => 'getProduct',
'summary' => 'Fetch one product',
'parameters' => [
['name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'string']],
],
'responses' => [
'200' => ['description' => 'Product found'],
'404' => ['description' => 'Product not found'],
],
],
],
],
];
echo $document['openapi'] . ' ' . array_key_first($document['paths']) . PHP_EOL;
// Prints:
// 3.2.0 /products/{id}
The openapi field states the specification version used by the document, not the product API version. The API version usually belongs in info.version, URL paths, headers, or release documentation.
Official specification: https://spec.openapis.org/oas/latest.html
Describe Status Semantics, Not Only Shapes
A schema tells clients what the JSON looks like. Status codes tell clients what happened.
Document expected outcomes separately:
200 OKfor a successful read or update with a body201 Createdfor creation, often withLocation202 Acceptedfor queued work204 No Contentfor success without a body400 Bad Requestfor malformed syntax401 Unauthorizedfor missing or invalid authentication403 Forbiddenfor authenticated callers without permission404 Not Foundfor absent resources409 Conflictfor state, uniqueness, or idempotency conflicts412 Precondition Failedfor failed optimistic concurrency checks422 Unprocessable Contentfor validation where the API uses that convention429 Too Many Requestsfor rate limits503 Service Unavailablefor temporary service failure
Do not document one generic error for every failure. Clients need to know which failures require user correction, new credentials, retry, conflict resolution, or support escalation.
Schemas Need Semantics
Schemas should define required properties, nullable values, string formats, enum values, numeric bounds, array item types, unknown-property rules, and examples. Required and nullable are different: a property can be required and allowed to be null, or optional but non-null when present.
Do not reuse one schema for create requests, patch requests, storage entities, and responses if their rules differ. A create request may not accept an ID. A patch request may make every field optional. A response may include timestamps, links, and server-generated state.
<?php
declare(strict_types=1);
$productSchema = [
'type' => 'object',
'required' => ['id', 'name', 'price'],
'properties' => [
'id' => ['type' => 'string', 'example' => 'prod_123'],
'name' => ['type' => 'string', 'minLength' => 1],
'price' => [
'type' => 'object',
'required' => ['amount_minor', 'currency'],
'properties' => [
'amount_minor' => ['type' => 'integer', 'minimum' => 0],
'currency' => ['type' => 'string', 'example' => 'GBP'],
],
],
],
];
echo implode(', ', $productSchema['required']) . PHP_EOL;
// Prints:
// id, name, price
Formats such as date-time, uuid, and email communicate intent, but tools differ in strictness. Add prose and examples for timezone, precision, canonicalization, and large identifiers when interoperability matters.
Reusable components are useful only when they preserve meaning. A shared User schema that is used for public profile responses, internal admin responses, and account update requests will usually leak fields or hide validation differences. Split components by role when the contract differs: UserProfileResponse, AdminUserResponse, CreateUserRequest, and UpdateUserRequest may look repetitive, but the names tell reviewers which side of the boundary they are checking.
Review $ref usage carefully. References reduce duplication, but they can also make a small change surprisingly broad. Tightening a reused Address component might break checkout, billing, warehouse, and support endpoints at the same time. Before changing a component, search every operation that references it and decide whether the change is compatible for all of them. If only one operation needs the stricter rule, create a narrower component instead of changing the common one.
Parameter schemas need the same attention as JSON bodies. A query parameter documented as string but implemented as a comma-separated list, a cursor, or an enum is underspecified. Path parameters should state their real format and stability. Header names should use the casing convention from the documentation, while implementations should still compare them case-insensitively according to HTTP rules.
Model Errors Consistently
Define an error envelope once and reuse it. A practical API error contains a stable machine code, human-safe message, optional field or parameter path, and request or correlation ID.
<?php
declare(strict_types=1);
$errorExample = [
'errors' => [[
'code' => 'invalid_parameter',
'parameter' => 'sort',
'message' => 'Sort field is not supported.',
'request_id' => 'req_abc123',
]],
];
echo json_encode($errorExample, JSON_THROW_ON_ERROR) . PHP_EOL;
// Prints:
// {"errors":[{"code":"invalid_parameter","parameter":"sort","message":"Sort field is not supported.","request_id":"req_abc123"}]}
Avoid exposing stack traces, SQL, internal class names, provider payloads, or secrets. Document authentication and authorization errors separately. For rate limits, document relevant headers such as Retry-After when the API sends them.
Security Schemes Are Not Authorization Policy
OpenAPI can describe bearer tokens, API keys, OAuth flows, Basic authentication, and operation-level security requirements. It cannot prove that GET /orders/{id} checks tenant ownership correctly.
Use security schemes to tell clients how to authenticate and which scopes are required. Use operation descriptions and separate authorization documentation to explain ownership, roles, and business restrictions at the level clients need.
Never put real credentials in examples. Use clearly fake tokens, reserved domains, and non-production server URLs.
Pagination, Concurrency, And Idempotency Are Contract Details
Collection operations should document page-size bounds, cursor or offset parameters, stable ordering, and response links. If a cursor is opaque, examples should not encourage clients to parse or construct it.
Update operations should document optimistic-concurrency headers such as ETag and If-Match where used, including 412 Precondition Failed. Creation or command operations should document idempotency key header name, retention period, replay response, request-equivalence rule, and conflict behavior.
These details are part of the API contract even though they are not simply resource fields.
Examples Must Be Valid
Examples are often the first thing a client developer copies. They should match schemas, status codes, headers, and real field names. Include at least one error example, not only the happy path.
Validate examples in CI where possible. A plausible example with the wrong date format or missing required property is worse than no example because it teaches clients the wrong contract.
Interactive renderers such as Swagger UI should be protected when the API is private. They reveal routes, schemas, authentication expectations, and server URLs. A "try it" console can also send real requests if pointed at production.
Prevent Specification Drift
Documentation drift happens when code and contract disagree. Common causes include renamed response fields, new validation rules, changed status codes, stale examples, undocumented headers, and error shapes that differ by endpoint.
Teams reduce drift by choosing one source of truth and enforcing it:
- write the OpenAPI document first and generate validation or stubs
- generate the document from code annotations and review the artifact
- validate real responses against schemas in integration tests
- run breaking-change checks against the last published spec
- fail CI when generated docs differ from committed docs
- review API docs in every endpoint pull request
A passing schema comparison is not a complete compatibility proof. Changing default sort order, pagination behavior, authorization semantics, retry behavior, or idempotency policy can break clients without changing JSON types.
Generated Clients And Server Stubs
OpenAPI can generate clients and server stubs, but generated code should sit behind an application boundary. Pin generator versions and templates. Review generated diffs. Do not edit generated files manually; fix the spec or generator configuration and regenerate.
Generated clients vary in how they represent nullable fields, date-time values, enums, one-of schemas, large integers, and unknown fields. Test the generated client against a real or contract-test server before publishing it to users.
Server stubs are not business logic. They do not implement authorization, transactions, idempotency, or domain rules by themselves.
Versioning And Publication
Publish a specification for each supported API version and tie it to a release identifier. Clearly label preview or development documents that are ahead of production.
Use deprecation fields and descriptions to communicate migration intent, but do not rely on a flag alone. Announce timelines, provide examples, measure old usage, and keep old contracts available until the support window ends.
When a breaking change is unavoidable, compare the new document with the released one and explain the migration path. Removing an operation, required response field, enum value, accepted input, or status response can break clients.
What To Check In A Project
Check which OpenAPI version the document uses and whether tooling supports it.
Check whether OpenAPI is the source of truth or generated from code.
Check that every public endpoint documents success, validation, authentication, authorization, not-found, conflict, rate-limit, and server-error outcomes where relevant.
Check that schemas match actual JSON, examples are valid, and sensitive fields are absent.
Check security schemes, pagination, idempotency, concurrency headers, and deprecation policy.
Check generated clients and contract tests against a deployed or realistic test environment.
What You Should Be Able To Do
After this lesson, you should be able to explain what OpenAPI documents, identify operations, parameters, request bodies, responses, schemas, examples, security schemes, and reusable components, and distinguish OpenAPI version from product API version.
You should also be able to review an API contract for missing status codes, weak schemas, stale examples, undocumented authentication, pagination or idempotency gaps, drift risk, and breaking changes before clients are affected.
Practice
Task: Sketch An OpenAPI Operation
Write a small PHP array that represents the important parts of an OpenAPI operation for GET /products/{id}.
Requirements
- Use
declare(strict_types=1);. - Include method and path.
- Include a required path parameter named
id. - Include documented
200and404responses. - Include a product example for the
200response. - Print a short summary of the operation.
Check Your Work
Run the script and confirm the summary mentions the path, parameter, and response statuses.
Show solution
This solution keeps the operation small but includes the parts clients need most.
<?php
declare(strict_types=1);
$operation = [
'method' => 'GET',
'path' => '/products/{id}',
'parameters' => [
['name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'integer']],
],
'responses' => [
'200' => [
'description' => 'Product found',
'example' => ['data' => ['id' => 123, 'name' => 'Notebook']],
],
'404' => [
'description' => 'Product not found',
'example' => ['errors' => [['code' => 'not_found', 'message' => 'Product not found.']]],
],
],
];
echo $operation['method'] . ' ' . $operation['path'] . PHP_EOL;
echo 'Parameter: ' . $operation['parameters'][0]['name'] . PHP_EOL;
echo 'Responses: ' . implode(', ', array_keys($operation['responses'])) . PHP_EOL;
// Prints:
// GET /products/{id}
// Parameter: id
// Responses: 200, 404
Why This Works
The operation tells clients how to call the endpoint and what success and missing-resource responses look like.
Practice: Review An OpenAPI Operation
Review a proposed POST /payments operation before it is published.
Task
List what the operation must document for clients to implement it safely. Include:
- request body fields and validation
- success status and response body
Idempotency-Keyheader behavior- authentication and authorization
- validation, conflict, rate-limit, and server-error responses
- examples
- retry guidance
Then identify two omissions that would make generated clients misleading.
Show solution
The operation should document the payment create request body, including amount minor units, currency, recipient or invoice reference, required fields, numeric bounds, and unknown-field behavior. It should document the success status, such as 201 Created, the response body, and any Location header.
It needs the Idempotency-Key header: whether it is required, key format, scope, retention period, request fingerprint behavior, replayed response, in-progress behavior, and 409 conflict when the same key is reused with different input.
Authentication should identify the scheme and required scope. Authorization prose should explain ownership or account restrictions. Responses should include examples for validation failure, authorization failure, conflict, rate limit with Retry-After if used, and temporary server failure.
Generated clients become misleading if the spec omits the required idempotency header or documents only 200 success while the API actually returns 201, 409, 422, 429, and 503. The generated method may look simple but encourage unsafe retries and incomplete error handling.
Practice: Plan OpenAPI Drift Controls
A team keeps finding differences between Laravel endpoint behavior and its OpenAPI YAML.
Task
Design a CI and review process that checks:
- document linting
- example validity
- generated artifact freshness
- real response validation against schemas
- breaking-change comparison against the last release
- generated client smoke tests
- pull-request review responsibilities
State one thing schema validation cannot prove.
Show solution
CI should lint the OpenAPI document, validate bundled examples against their schemas, and regenerate any generated artifact from a clean checkout. If generated output differs from committed output, the build fails.
Integration tests should call representative endpoints in a test environment and validate status, headers, and body against the documented operation. A breaking-change tool should compare the pull request document with the last published release and flag removed fields, removed operations, new required inputs, changed types, or enum changes.
A generated client smoke test should call a mock or test server for important happy and error paths. Pull requests that change routes, validation, resources, authentication, pagination, idempotency, or errors must include OpenAPI review from the endpoint owner and, for public APIs, someone thinking from the client side.
Schema validation cannot prove authorization semantics. A response can match the schema while leaking another tenant's data or using the wrong permission rule.