Process and tooling
This page explains how the Reading List API documentation was built and why I made the choices I did. The workflow is OpenAPI-first: I wrote the API description before any other page, generated the reference from it, and wrote the guides by hand around it.
Goal
I wanted one sample that shows both halves of modern API documentation: structured, tool-friendly source material that generators and mock servers can consume, and human-written guides that no generator produces. The API is small on purpose, so that every page can be finished and checked against the others.
Tooling
| Tool | Version | Used for |
|---|---|---|
| OpenAPI Specification | 3.1.0 | Describing the API |
| Redocly CLI | 2.63.0 | Linting the specification and generating the HTML reference |
| Prism | 5.16.0 | Running a local mock server from the specification |
| MkDocs with Material for MkDocs | n/a | Publishing the guides |
| Git and GitHub Pages | n/a | Source control and hosting |
I pinned the Redocly CLI and Prism versions in every command rather than using @latest, so the output is repeatable and a future release cannot change the results unexpectedly.
Decisions and tradeoffs
A fictional API with three endpoints. Searching for a real, undocumented API would have cost weeks and left me documenting behavior I could not verify. A fictional API I control lets me document every behavior and check it. Each endpoint exists to demonstrate something specific: GET /books covers query parameters, an enum filter, and a default value; GET /books/{bookId} covers a path parameter and a 404; POST /books covers a request body, validation, and a 201.
OpenAPI 3.1. Every tool in this workflow accepted 3.1 without conversion: the same file lints in Redocly CLI, generates a reference, and drives a Prism mock.
Specification first. Writing the YAML before the guides meant every example in the guides could be copied from the specification instead of invented separately. When an example changed, I changed it in one place.
Problem Details for errors. RFC 9457 gives every error the same machine-readable shape, which makes the error guide shorter and the client code simpler. The standard allows extension members, so I added an errors array that names each invalid field. A bare "the request body is invalid" does not tell a developer what to fix.
API key authentication. It is the simplest scheme that I could document completely, including key handling and rotation. A scheme like OAuth would have added flows that a local mock cannot demonstrate.
One copy of the specification. The YAML lives in the same folder as the pages. MkDocs publishes it unchanged for download, and the generator reads the same file, so the downloadable spec and the generated reference cannot drift apart.
Workflow
Run from the root of the portfolio repository:
# 1. Validate the specification
npx @redocly/cli@2.63.0 lint docs/projects/reading-list-api/reading-list-api.yaml
# 2. Generate the HTML reference
npx @redocly/cli@2.63.0 build-docs docs/projects/reading-list-api/reading-list-api.yaml \
--output docs/projects/reading-list-api/api-reference.html
# 3. Build and preview the site
mkdocs serve
A small script, scripts/build-reading-list-api.sh, runs all of this in order and stops at the first failure. I rerun it whenever the specification changes, because the generated HTML does not update itself.
What I wrote and what was generated
| Content | Source |
|---|---|
| API structure: paths, parameters, schemas, status codes | Written by hand in the OpenAPI file |
| Descriptions, examples, validation rules, code samples | Written by hand in the OpenAPI file |
Interactive reference page (api-reference.html) |
Generated by Redocly CLI from the OpenAPI file |
| Mock server responses | Generated by Prism from the OpenAPI examples |
| Overview, Quickstart, Authentication, and Error handling guides | Written by hand |
| Changelog and this page | Written by hand |
The generated reference is only as good as the descriptions in the specification. Generating it took one command. Writing field descriptions, examples, and the validation rules took most of the effort, and that is the part a reader sees.
What linting caught
My first draft of the specification passed validation but produced four warnings from Redocly CLI's recommended rules: a missing license in the info object, and a missing operationId on each of the three operations. I added them. The operationId values (listBooks, createBook, getBook) are also the names that code generators and tools such as Postman use.
Linting checks structure, not quality. It did not flag that the first draft had no descriptions on its schema fields, no query parameters, and only a one-line 400 example. I found those by reading the generated reference as a developer would, and then fixed them.
What the mock server does and does not do
Prism builds a mock from the specification. It is a good way to prove that the examples are well formed and the documentation is usable, but it is not a working API. I tested each behavior below against Prism 5.16.0 and recorded what it actually did.
| Behavior | The API as designed | The Prism mock |
|---|---|---|
Request without an X-API-Key header |
401 |
401 |
| Request with an unrecognized key | 401 |
200: accepts any value |
| Key sent as a query parameter | 401 |
401: the header is still missing |
| Unknown book ID | 404 |
200 with an example book, unless the request sets Prefer: code=404 |
status filter on GET /books |
Returns only matching books | Ignored |
limit on GET /books |
Caps the number of books | Ignored, but values outside 1 to 100 return 400 |
POST /books |
Stores the book and assigns an id |
Returns a fixed example; nothing is stored |
| Invalid request body | 400 listing the fields that were invalid |
400 with a fixed example body |
Testing the mock also exposed a mistake in my own specification. My first version of GET /books reused the request-body 400 example, so a bad limit value appeared to produce an error about a missing title. I gave the operation its own example.
Because the mock behaves differently from a real server, the guides say so wherever it matters. A reader who follows the Quickstart should never be surprised by a result that the page did not warn them about.
Limitations and next steps
This version is intentionally small. If I were extending it, I would add:
PATCH /books/{bookId}to update a reading status. Today a book's status can be set only when it is created, which is the most obvious gap in the API.DELETE /books/{bookId}to remove a book.- Pagination.
limitcaps the list, but there is no way to request the next page. Cursor-based pagination would fix that. - Rate limiting, with a
429response and documented headers. - Error
typepages. ThetypeURIs are placeholders, and a production API would document each one. - Automated checks that run the code samples in CI, so the documentation fails the build when an example stops working.