Written by
Fuma Nama
At
Sun Sep 20 2026
Fumadocs OpenAPI v12
Headless API pages, and a UI you own.
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.
| Before | Now |
|---|---|
ParsedSchema | JsonSchema |
NoReference / NoReferenceSwallow | Dereferenced / DereferencedShallow |
dereferenceShallow(schema) | dereference(schema) |
matchesSchema(schema, value) | matches(schema, value) |
typeMatches(value, type) | matchesType(value, type) |
schemaToString(schema, FormatFlags.UseAlias) | stringify(schema, { alias: true }) |
| Before | Now |
|---|---|
@fumadocs/api-docs/schema/* | @fumadocs/json-schema |
@fumadocs/api-docs/components/schema* | npx @fumadocs/cli add api-docs/schema |
@fumadocs/api-docs/components/* (the UI) | installed with the component that uses it |
@fumadocs/api-docs/i18n | the integration's own Translations covers its keys |
@fumadocs/api-docs/css/preset.css | already included by the integration's preset |
The CLI still installs the Schema UI, now under api-docs/schema.
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
| Removed | Use |
|---|---|
fumadocs-openapi/ui/create-client | createOpenAPIPage() from fumadocs-openapi/ui |
ApiPageProps | OpenAPIPageProps |
OperationItem and WebhookItem of fumadocs-openapi/ui | import them from fumadocs-openapi |
getAPIPageProps() and getClientAPIPageProps() | getOpenAPIPageProps() |
defineI18nOpenAPI() | i18n.translations().extend(openapiTranslations()) |
APIPage of MDX components | OpenAPIPage, generated files only render it |
| Was | Now |
|---|---|
fumadocs-openapi/playground/client | fumadocs-openapi/ui/playground/client |
fumadocs-openapi/scalar | fumadocs-openapi/ui/scalar |
GenerateTypeScriptDefinitionsContext comes from the package entry, next to the runtime option it types.
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 :)