Generating API Reference Pages in Starlight From OpenAPI
API reference is the part of product documentation readers use most and writers enjoy least. Every endpoint needs its method, path, parameters, request body, responses and examples, and every one of those changes when the API does. Writing them by hand guarantees drift. The alternative is to treat the OpenAPI specification as the source and generate the reference pages from it at build time, so the docs are exactly as accurate as the spec — and to put the writers' effort into the spec's descriptions and into guides that link to the generated pages.
This guide generates reference pages for a payments API — 240 operations across 18 tags, 85 schemas, OpenAPI 3.1 — inside a Starlight 0.30 documentation site, using the starlight-openapi plugin, then adds spec validation in CI, cross-links between guides and reference, and an optional try-it console. It belongs to Docs Frameworks: Docusaurus, Starlight and VitePress; the selection side is covered in Choosing an SSG for API Reference Documentation.
Prerequisites
- An OpenAPI 3.0 or 3.1 specification in YAML or JSON that validates.
- A Starlight site, Astro 5 or later.
- Access to where the spec is published: a URL, a package, or a file in the docs repository.
How the Pieces Fit
Step 1: Fetch a Pinned Spec
The docs document a specific API version, so the build fetches the spec for that version rather than whatever is newest:
// scripts/fetch-spec.mjs (runs before astro build)
const version = process.env.API_VERSION ?? '2026-09-01';
const res = await fetch(`https://artifacts.example.com/payments-api/${version}/openapi.yaml`);
if (!res.ok) throw new Error(`spec ${version}: ${res.status}`);
await fs.writeFile('src/spec/openapi.yaml', await res.text());
API_VERSION is set in the docs repository and bumped in the same pull request that documents a new release, so guides and reference change together.
Step 2: Generate the Pages
npm install starlight-openapi
// astro.config.mjs
import starlight from '@astrojs/starlight';
import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi';
export default defineConfig({
integrations: [
starlight({
title: 'Payments Docs',
plugins: [starlightOpenAPI([{ base: 'api', label: 'API reference', schema: './src/spec/openapi.yaml' }])],
sidebar: [
{ label: 'Guides', autogenerate: { directory: 'guides' } },
...openAPISidebarGroups,
],
}),
],
});
The plugin generates an overview page from the spec's info, one page per operation under /api/operations/<operationId>/, and one per schema, grouped in the sidebar by tag. Descriptions in the spec are rendered as Markdown, so writers can use links, lists and code in them.
Step 3: Write Better Descriptions in the Spec
The first generated pages showed how thin the spec's prose was: 96 of 240 operations had a one-line summary and no description, and 310 of 1,100 parameters had none at all. Generated reference is only as good as the spec, so the documentation team started contributing to the spec in the API repository. Two lint rules made it stick: Redocly's operation-description and a custom rule requiring descriptions on every parameter and schema property, both set to warn and then error.
# redocly.yaml in the API repository
rules:
operation-description: error
operation-summary: error
rule/parameter-description:
subject: { type: Parameter, property: description }
assertions: { defined: true, minLength: 20 }
severity: error
Over six weeks, operations with descriptions went from 144 to 240 and parameters from 790 to 1,100.
Step 4: Link Guides to Operations Safely
Guides refer to endpoints constantly ("create a refund with POST /refunds"). Linking by URL breaks when an operationId changes, and nothing would notice. A small remark plugin resolves a custom link syntax at build time and fails on unknown IDs:
Create the refund with [`POST /refunds`](op:createRefund).
// remark-op-links.mjs
const ops = new Set(Object.values(spec.paths).flatMap((p) => Object.values(p).map((o) => o.operationId)));
visit(tree, 'link', (node) => {
if (!node.url.startsWith('op:')) return;
const id = node.url.slice(3);
if (!ops.has(id)) file.fail(`Unknown operationId "${id}"`, node);
node.url = `/api/operations/${id.toLowerCase()}/`;
});
When the API team renamed four operations in a release, the docs build failed on the seven guide links that used them, in the pull request that bumped API_VERSION.
Step 5: Add an Optional Try-It Console
Some readers want to send a request from the page. A try-it panel built as a small Preact island, loaded with client:visible and only when the reader clicks "Try it", sends requests to the sandbox environment with a test key the reader pastes in. It adds 19 KB of JavaScript on the operation pages, and only when opened; the static content is complete without it. Keeping it optional means the reference stays fast and indexable, a trade-off discussed in Deferring Hydration with client:visible in Astro.
Step 6: Validate in CI on Both Sides
Two checks keep the reference correct:
- In the API repository, every pull request lints the spec with Redocly and runs
oasdiffagainst the last release to flag breaking changes, which must be labelled and documented. - In the docs repository, the build fails on an unresolvable spec, unknown
op:links, or a spec that fails Redocly's lint, so a bad spec cannot publish broken reference.
Customising the Generated Layout
The plugin's default operation page lists parameters, request body and responses in that order. Readers of this API most often wanted an example request first, so the site overrides Starlight's MarkdownContent component for pages under /api/ to move the first x-codeSamples entry from the spec above the parameter tables, rendered as Starlight tabs for curl, Node and Python. The override is about 50 lines and does not fork the plugin; it reads the operation from the page's route data. A second small change shows the operation's required OAuth scopes as a badge next to the method and path, because support tickets showed readers missing the scope requirement buried in the security section.
Performance of the Generated Pages
| Measure | Value | Tool |
|---|---|---|
| Pages generated | 240 operations, 85 schemas, 1 overview | Build output |
| Build time added | 9 s (38 s → 47 s) | hyperfine |
| Operation page, transferred | 41 KB (19 KB more when try-it opened) | DevTools |
| LCP, operation page, mobile | 1.0 s | Lighthouse 12 |
| Search | Pagefind indexes reference pages with guides | Build output |
The generated pages are ordinary Starlight pages, so they get the same theme, search and dark mode as the guides.
Measured Impact
Compared with the hand-maintained reference the site had before:
| Measure | Hand-written | Generated | Source |
|---|---|---|---|
| Endpoints documented | 201 of 240 | 240 of 240 | Spec vs pages |
| Reference errors reported by readers, per quarter | 23 | 2 | Support tickets |
| Writer hours on reference, per release | ~14 | ~3 (spec descriptions) | Time tracking |
| Broken guide links to endpoints after a release | 5–10 | 0 (build fails) | Link checker |
Pitfalls & Rollback
- Generating from an unpinned spec. Docs then describe an API that has not shipped; pin the version.
- Accepting thin spec descriptions. Generated pages expose them; lint for them in the API repository.
- Linking guides to reference by URL. Use operation IDs resolved at build time.
- Making the try-it console load by default. Keep it an opt-in island.
- Rollback: remove the plugin and its sidebar groups; guides'
op:links would then fail the build, so switch the remark plugin to emit plain text before removing the reference.
Conclusion
Generating Starlight reference pages from a pinned OpenAPI spec makes the reference exactly as accurate as the spec, and moves writers' effort to where it pays: descriptions in the spec and guides that link to operations by ID. For a 240-operation payments API, it documented every endpoint, cut reader-reported reference errors from 23 a quarter to 2, and added 9 seconds to the build.
FAQ
Should API reference pages be generated or written by hand?
Generated, from the same OpenAPI specification the API is built or tested against. Hand-written reference drifts from the real API within weeks. Generated pages are always as accurate as the spec, and writers spend their time on guides, examples and descriptions inside the spec rather than on tables of parameters.
Does generating hundreds of operation pages slow the Starlight build?
Somewhat. On the site in this guide, 240 operation pages and 85 schema pages added 9 seconds to a 38-second build. The cost is in rendering, not parsing the spec, and it scales with the number of operations. Splitting a very large spec into tagged groups keeps sidebars usable but does not change build time much.
Can I use an interactive API console instead of static pages?
You can embed one, but static pages should come first. They are searchable, indexable, readable without JavaScript and fast. An interactive try-it console works well as an optional island on each operation page, loaded only when the reader asks for it.
How do I keep the spec and the docs in sync?
Store the spec in the API's repository as the source of truth, publish it as a build artifact or package on each release, and have the docs build fetch the version it documents. A CI check that lints and diffs the spec on every API pull request catches undocumented changes before release.
Related
- Parent: Docs Frameworks: Docusaurus, Starlight and VitePress — what each framework offers for reference docs.
- Choosing an SSG for API Reference Documentation — the selection criteria.
- Customizing Starlight Without Forking the Theme — adjusting generated pages' layout.
- Loading Remote Content With Astro Content Layer Loaders — another way to bring external data into an Astro build.
- Testing Structured Data and Meta Tags — checks that also apply to generated pages.