We are pleased to announce the release of Fumadocs OpenAPI v12.
An API page used to be one component with a long list of render options. v12 splits it in two:
- a headless layer that owns the state and logic,
- the UI it renders through, which you can install and edit.
Options remain for the small tweaks, the rest is yours to replace.
Upgrading from v10?
Read the Fumadocs OpenAPI v11 announcement first.
Headless API Pages
Everything outside fumadocs-openapi/ui renders nothing on its own.
| Path | Module |
|---|---|
fumadocs-openapi | the page: the document, its servers and your components |
fumadocs-openapi/operation | an operation: its details, example requests and code usages |
fumadocs-openapi/playground | the auth state and the fetcher of the playground |
fumadocs-openapi/requests | encoding the data of an example into a real request |
fumadocs-openapi/requests/generators | turning that request into code usages |
createOpenAPIRenderer() takes your components and returns an <OpenAPIPage /> that accepts the props of generated pages:
'use client';
import { createOpenAPIRenderer } from 'fumadocs-openapi';
import { Operation } from '@/components/my-operation';
import { SchemaUI } from '@/components/my-schema';
export const OpenAPIPage = createOpenAPIRenderer({
components: { Operation, SchemaUI },
});Markdown, CodeBlock and Heading are filled in when you leave them out, so a custom UI starts from two components.
Under the page, hooks read its state:
| Hook | Returns |
|---|---|
useOpenAPI() | the dereferenced document (doc) and request options |
useComponents() | the components passed to the page |
useServer() | the selected server, its variables, and resolveUrl(pathname) |
useOperation() | the operation with its details resolved |
useExampleRequests() | the example requests, the selected one, select() and update() |
useCodeUsage(id) | the code generated for the selected example |
useResponseExamples() | responses with example values of their preferred media type |
See Headless for the full surface.
Install the UI
The entire UI of API pages, built on that layer, installed with Fumadocs CLI:
npx @fumadocs/cli add openapi/pageYour components/api-page.tsx is no longer needed:
import { OpenAPIPage } from '@/components/api-page';
import { OpenAPIPage } from '@/components/openapi/page'; Or take one part of it, and pass the rest through components:
'use client';
import { createOpenAPIPage } from 'fumadocs-openapi/ui';
import { Operation } from '@/components/openapi/operation';
import { Schema } from '@/components/api/schema';
export const OpenAPIPage = createOpenAPIPage({
components: { Operation, SchemaUI: Schema },
});| Part | Install with |
|---|---|
| Full page | openapi/page |
| Operation UI | openapi/operation |
| API Playground | openapi/playground |
| Schema UI | api-docs/schema |
What you install is UI, not a fork:
- the request pipeline of the playground stays in the package, an edited playground still drives the real one.
SelectandInputfollow the Shadcn UI API, a project that already has them keeps its own.
@fumadocs/json-schema
The JSON Schema utilities of API pages are their own package, with no Fumadocs dependencies:
import { dereference, matches, mergeAllOf, sample, stringify } from '@fumadocs/json-schema';
import { bundle } from '@fumadocs/json-schema/bundle';bundle()is a separate entry because it reads files and URLs, everything else runs in the browser.@fumadocs/json-schema/reactturns a schema into the data an API page draws.generateSchemaUI()flattens it, so your UI walks a map from$rootinstead of recursing.@fumadocs/api-docsis no longer published, its UI is now bundled into the integrations or installed with the CLI.
Smaller Bundles
createOpenAPIBaseRenderer() is the renderer with nothing built in, it bundles only what you pass:
createOpenAPIBaseRenderer({
shiki,
codeUsages: createCodeUsageGeneratorRegistry().register(curl),
components: { Operation, SchemaUI },
});createOpenAPIRenderer() and fumadocs-openapi/ui register the full Shiki bundle, every code usage generator and TypeScript definitions for you.
The package entry is client-safe too. generateFiles() reads and writes files, so it ships stubbed under the browser condition, and a client component importing the renderer no longer pulls node:fs into the bundle.
Migration
Run a type check after upgrading:
npm run types:checkComponents installed from v11 with Fumadocs CLI, like the API playground, use the old hooks. Reinstall them.
Options
Render options that returned a component are components now:
createOpenAPIPage({
- schemaUI: { render: (props) => <Schema {...props} /> },
- renderHeading: (props, depth) => <Heading depth={depth} {...props} />,
- renderCodeBlock: (props) => <CodeBlock {...props} />,
- renderMarkdown: (md) => <Markdown md={md} />,
+ components: { SchemaUI: Schema, Heading, CodeBlock, Markdown },
});-
playground.provideris removed, the page provides the auth state of the playground. -
playground.renderandgenerateTypeScriptDefinitionsno longer receivectx. Read the document fromuseOpenAPI().doc, or thedocpassed togenerateTypeScriptDefinitions. -
operation.APIExampleSelectoris removed, installopenapi/operationand edit the selector inusage-tabs.tsx. -
<PlaygroundClient />reads the operation fromuseOperation(), itsroute,method,operationandpathItemprops are gone:playground: { render: () => <PlaygroundClient writeOnly readOnly={false} />, } -
The
ctxofcontentrender options is the render options of the page, whatuseRenderContext()returns.ctx.schema,ctx.SchemaUIandctx._default_processMarkdownare removed, read them fromuseOpenAPI().docanduseComponents().
Hooks
The hooks of fumadocs-openapi/ui are replaced by the headless ones:
| v11 | v12 |
|---|---|
useRenderContext() | useOpenAPI() (schema is renamed to doc), useComponents(), useRenderContext() |
useServerContext() | useServer() |
useOperationContext() | useOperation(), useExampleRequests(), useExampleRequest() (on /operation) |
- const { route, examples, example, setExample, setExampleData } = useOperationContext();
+ const { path } = useOperation();
+ const { items, selected, select, update } = useExampleRequests();
+ // data of the selected example, replaces `addListener()`
+ const data = useExampleRequest();useStorageKey() is removed, read the prefix from the page:
const { storageKeyPrefix } = useOpenAPI();
localStorage.getItem(`${storageKeyPrefix}my-key`);Removed & Moved
Also in v12
- Code samples from
x-codeSamplesandgenerateCodeSamplesare rendered when their id isn't a built-in generator. SchemaData.infoTagsentries are data the UI renders:{ label, value, block? }or{ label, list }. Custom nodes ({ node }) still work, code readingtag.nodemust handle all shapes.
Thanks for supporting Fumadocs, share your upgrade experience on GitHub :)