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

Pipeline from the OpenAPI spec in the API repository to generated Starlight pages Five steps. The API repository holds openapi.yaml as the source of truth. On release, CI lints the spec with Redocly and publishes it as an artifact with the version. The docs build fetches the published spec for the version it documents. The Starlight OpenAPI plugin generates one page per operation and per schema, plus sidebar groups by tag. Guides link to operations by operationId, checked at build time. From API repository to reference pages openapi.yaml in API repo Lint and publish per release Docs build fetches pinned version Generate pages operations, schemas Guides link by ID checked at build
The docs never edit the spec; improvements to descriptions go to the API repository as pull requests.

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.

Share of operations and parameters with descriptions before and after Paired bars. Operations with a description went from 60 percent to 100 percent. Parameters with a description went from 72 percent to 100 percent. Schemas with examples went from 31 percent to 88 percent. Spec coverage before and after the lint rules Before After Operations with description 60% 100% Parameters with description 72% 100% Schemas with examples 31% 88%
Measured with a script over the spec at the start and after six weeks of pull requests to the API repository.

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:

  1. In the API repository, every pull request lints the spec with Redocly and runs oasdiff against the last release to flag breaking changes, which must be labelled and documented.
  2. 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.
Problems caught by checks in the API and docs repositories A matrix of four problems against two places checks run. A missing description is caught by the lint in the API repository and by the lint in the docs build. A breaking change is caught by oasdiff in the API repository. A renamed operation used by a guide is caught by the op link check in the docs build. An invalid spec is caught in both places. Which check catches which problem Problem API repository Docs build Missing description Redocly lint lint again Breaking change oasdiff not checked Renamed operation in a guide not visible op: link check Invalid spec lint fails build fails
Problems are caught as close as possible to the change that caused them.

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

MeasureValueTool
Pages generated240 operations, 85 schemas, 1 overviewBuild output
Build time added9 s (38 s → 47 s)hyperfine
Operation page, transferred41 KB (19 KB more when try-it opened)DevTools
LCP, operation page, mobile1.0 sLighthouse 12
SearchPagefind indexes reference pages with guidesBuild 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:

MeasureHand-writtenGeneratedSource
Endpoints documented201 of 240240 of 240Spec vs pages
Reference errors reported by readers, per quarter232Support tickets
Writer hours on reference, per release~14~3 (spec descriptions)Time tracking
Broken guide links to endpoints after a release5–100 (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.

Filed underHow-tosStarlight