Integrating Docusaurus with OpenAPI Specs
Wiring an OpenAPI specification into Docusaurus is the most common reason teams pick it for an API portal, but the moving parts trip people up: a plugin that reads the spec and emits MDX, a theme that renders that MDX, and a validation gate that stops a broken spec from shipping a broken site. This guide is part of Docusaurus for API Portals and the wider Developer Portal Frameworks & UI Setup section. It walks through exact configuration, sidebar generation, and the CI steps that keep generated docs honest.
Target stack: Docusaurus v3.x with OpenAPI 3.0.3 or 3.1.0. Legacy 2.0 specs must be converted before ingestion. The scenario this page solves arises the moment your spec stops being a single hand-written page and becomes the source of truth that the portal must mirror automatically.
Problem & Context
Without an automated bridge, a Docusaurus API portal drifts. Engineers ship a new endpoint, the spec updates, and the published reference still describes last quarter’s contract. Hand-copying operations into MDX does not scale past a handful of paths, and it introduces transcription errors that erode trust in the docs. The fix is to treat the OpenAPI spec as the single source of truth and regenerate MDX on every change, with a lint step that fails the build before bad output reaches readers.
The before-state is familiar: a gen-api-docs run throws TypeError: Cannot read properties of undefined (reading 'paths') because someone committed a Swagger 2.0 file, or the build dies with JavaScript heap out of memory on a spec with several hundred operations. Each of these has a precise cause and fix, covered below.
Step-by-Step Solution
1. Install the plugin and theme with exact pins
Pin both packages so a Docusaurus minor bump cannot drag in an incompatible peer dependency:
npm install --save-exact docusaurus-plugin-openapi-docs docusaurus-theme-openapi-docs
Expected output ends with a single added entry per package and no peer-dependency warnings. If npm reports ERESOLVE, your @docusaurus/core major version does not match the plugin’s supported range — align both on v3.x.
2. Register the plugin and theme
Add the plugin to the plugins array and the renderer to themes in docusaurus.config.ts:
// docusaurus.config.ts
import type { Config } from '@docusaurus/types';
const config: Config = {
// ... title, url, baseUrl, presets, etc.
plugins: [
[
'docusaurus-plugin-openapi-docs',
{
id: 'api-v1', // unique per spec; reused by gen-api-docs
docsPluginId: 'classic', // must match the @docusaurus/plugin-content-docs id
config: {
api: {
specPath: 'openapi/v1/api.yaml',
outputDir: 'docs/api/v1',
sidebarOptions: {
groupPathsBy: 'tag', // one category per OpenAPI tag
categoryLinkSource: 'tag', // category header links to the tag page
},
},
},
},
],
],
themes: ['docusaurus-theme-openapi-docs'],
};
export default config;
The docsPluginId: 'classic' value must equal the id of the @docusaurus/plugin-content-docs instance from your preset. The classic preset registers it as 'classic' by default, so leave it unless you renamed the docs instance.
3. Generate MDX from the spec
npm run docusaurus gen-api-docs api-v1
Expected output lists each created MDX file and a sidebar.js write into docs/api/v1. Start the dev server to confirm the nav tree appears:
npm run start
4. Gate the build with Spectral in CI
Validate before generating so a malformed spec fails fast rather than producing half-built pages:
# .github/workflows/docs-ci.yml
name: Docs CI
on:
pull_request:
push:
branches: [main]
jobs:
validate-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npx @stoplight/spectral-cli lint openapi/v1/api.yaml --ruleset spectral.yaml
- run: npm run docusaurus gen-api-docs api-v1
- run: npm run build
env:
NODE_OPTIONS: '--max-old-space-size=4096'
The NODE_OPTIONS value raises the V8 heap and prevents out-of-memory crashes on specs with more than ~500 operations. If you author Spectral rules from scratch, see Writing Custom Spectral Rules.
5. Control the sidebar from tags
The sidebarOptions block decides how endpoints group in the nav:
| Option | Value | Effect |
|---|---|---|
groupPathsBy |
'tag' |
One sidebar category per OpenAPI tags entry |
categoryLinkSource |
'tag' |
Category header links to the tag’s description page |
sidebarCollapsible |
true (default) |
Categories start collapsed |
No manual sidebars.js edits are needed; the plugin writes a sidebar.js into outputDir that Docusaurus picks up automatically.
6. Theme overrides and dark mode
Docusaurus uses Infima CSS custom properties. Override the generated theme in src/css/custom.css:
/* src/css/custom.css */
:root {
--ifm-color-primary: #0052cc;
}
[data-theme='dark'] {
--ifm-color-primary: #4c9aff;
/* Fix unreadable response-table stripes in dark mode */
--ifm-table-stripe-background: var(--ifm-background-color);
}
To replace a whole component, wrap it rather than eject it:
npm run docusaurus swizzle docusaurus-theme-openapi-docs ApiItem -- --wrap
--wrap preserves upstream updates; --eject forks the component permanently. For deeper theming, follow Multi-Theme & Dark Mode Support.
Complete Working Example
A single self-contained docusaurus.config.ts that ingests two spec versions, wires the theme, and adds the Node polyfills some parser dependencies need in browser bundles:
// docusaurus.config.ts
import type { Config } from '@docusaurus/types';
const config: Config = {
title: 'Acme API Portal',
url: 'https://docs.acme.dev',
baseUrl: '/',
presets: [
['classic', { docs: { sidebarPath: './sidebars.ts' } }],
],
plugins: [
[
'docusaurus-plugin-openapi-docs',
{
id: 'api-v1',
docsPluginId: 'classic',
config: {
api: {
specPath: 'openapi/v1/api.yaml',
outputDir: 'docs/api/v1',
sidebarOptions: { groupPathsBy: 'tag', categoryLinkSource: 'tag' },
},
beta: {
specPath: 'openapi/v2/api.yaml',
outputDir: 'docs/api/v2',
sidebarOptions: { groupPathsBy: 'tag', categoryLinkSource: 'tag' },
},
},
},
],
],
themes: ['docusaurus-theme-openapi-docs'],
// Resolve "Can't resolve 'fs'" errors during client bundling
configureWebpack(_config, isServer) {
if (!isServer) {
return {
resolve: {
fallback: {
fs: false,
path: require.resolve('path-browserify'),
os: false,
},
},
};
}
return {};
},
};
export default config;
Generate both trees with npm run docusaurus gen-api-docs api-v1 && npm run docusaurus gen-api-docs beta, then npm run build.
Gotchas & Edge Cases
TypeError: Cannot read properties of undefined (reading 'paths'). The spec has no top-levelpathskey, or it is a Swagger 2.0 file. Convert before generating:npx swagger2openapi openapi.yaml -o openapi-v3.yaml, then pointspecPathat the converted file.- Sidebar duplicates endpoints. A single path carries multiple tags, so it lands in several categories. Give each operation one primary tag, and use
x-tagGroupsin the spec when you need super-categories rather than extra tags per operation. Module not found: Error: Can't resolve 'fs'during client bundling. A parser dependency references a Node built-in. Add theconfigureWebpackfallback block shown in the example so browser bundles stubfs/osand shimpath.
FAQ
How do I handle OpenAPI 3.1.0 vs 3.0.3 compatibility?
Use docusaurus-plugin-openapi-docs v3.x or later, which reads both versions. If $ref resolution fails on a 3.1 keyword such as prefixItems, set the spec header back to openapi: "3.0.3" until the plugin adds full 3.1 coverage. The rest of your document stays unchanged.
Can I render multiple OpenAPI specs on one Docusaurus site?
Yes. Add one entry per spec inside the plugin’s config object, each with a unique key, specPath, and outputDir. Each entry emits its own MDX tree and sidebar file, so versioned references live side by side without colliding.
How do I stop stale spec output during local development?
Delete the outputDir and re-run gen-api-docs after every spec edit, since the plugin does not diff prior output. For a watch loop, run nodemon --watch openapi/ --exec "npm run docusaurus gen-api-docs api-v1" alongside npm run start so MDX regenerates on change while the dev server stays up.