Fumadocs

Headless

The state and logic of API pages, without UI.

Overview

Everything outside fumadocs-openapi/ui is headless.

PathModule
fumadocs-openapithe page: the document, its servers and your components
fumadocs-openapi/operationan operation: its details, example requests and code usages
fumadocs-openapi/playgroundan API playground: its form, auth and requests
fumadocs-openapi/requestsencoding the data of an example into a real request
fumadocs-openapi/requests/generatorsturning that request into code usages
@fumadocs/json-schema/reactthe Schema UI: JSON schemas flattened for UI
@fumadocs/json-schema/react/clientthe state of a Schema UI

Page

createOpenAPIRenderer() takes your components and returns <OpenAPIPage />, which accepts the props of generated pages.

components/api-page.tsx
'use client';
import { type CodeBlockProps, createOpenAPIRenderer } from 'fumadocs-openapi';
import { Operation } from '@/components/my-operation';
import { SchemaUI } from '@/components/my-schema';

export const OpenAPIPage = createOpenAPIRenderer({
  components: {
    Operation,
    SchemaUI,
    CodeBlock({ lang, code }: CodeBlockProps) {
      return (
        <pre>
          <code className={`language-${lang}`}>{code}</code>
        </pre>
      );
    },
  },
});
ComponentRenders
Operationeach operation and webhook, see Operation
SchemaUIthe JSON schemas of parameters, bodies and responses, see Schema UI
Layoutoptional, wraps the rendered operations and webhooks

Hooks

Components under the page read its state.

HookReturns
useOpenAPI()the dereferenced document (doc) and request options
useComponents()the components passed to the page
useServer()the selected server and its variables
useTypeScriptDefinitions(schema, options)TypeScript definitions of a JSON schema
useRenderContext()the render options of the page, like playground

Operation

<OperationProvider /> derives the details of an operation or webhook, and holds its selected example request.

components/my-operation.tsx
'use client';
import {
  OperationProvider,
  type PageOperationProps,
  useCodeUsage,
  useOperation,
  useResponseExamples,
} from 'fumadocs-openapi/operation';

export function Operation(props: PageOperationProps) {
  return (
    <OperationProvider {...props}>
      <Content />
    </OperationProvider>
  );
}

function Content() {
  const { title, parameters, codeUsages } = useOperation();
  const responses = useResponseExamples();

  return (
    <>
      <h2>{title}</h2>
      {parameters.map(({ in: location, items }) => (
        <section key={location}>{items.map((param) => param.name).join(', ')}</section>
      ))}
      {Array.from(codeUsages.map().keys(), (id) => (
        <CodeUsage key={id} id={id} />
      ))}
      {responses.map((tab) => (
        <pre key={tab.code}>{JSON.stringify(tab.examples?.[0]?.sample)}</pre>
      ))}
    </>
  );
}

function CodeUsage({ id }: { id: string }) {
  const code = useCodeUsage(id);
  return code && <pre>{code}</pre>;
}
HookReturns
useOperation()the operation with its details resolved
useExampleRequests()the example requests, the selected one, select() and update() to edit its data
useExampleRequest()data of the selected example, following its updates
useCodeUsage(id)the code generated by codeUsages for the selected example
useResponseExamples()responses with example values of their preferred media type

Prop

Type

Schema UI

generateSchemaUI() flattens a JSON schema: every $ref it reaches becomes an entry in refs, so your UI walks a map from $root instead of recursing.

<SchemaUIProvider /> holds the state on top of it: the schemas opened from properties, the selected members of unions, and links to a property.

components/my-schema.tsx
'use client';
import { useMemo } from 'react';
import { generateSchemaUI } from '@fumadocs/json-schema/react';
import { SchemaUIProvider, useSchemaUI } from '@fumadocs/json-schema/react/client';
import type { OpenAPIComponents } from 'fumadocs-openapi';

export const SchemaUI: OpenAPIComponents['SchemaUI'] = ({ client, ...options }) => {
  const generated = useMemo(() => generateSchemaUI(options), [options.root]);

  return (
    <SchemaUIProvider id={client.rootId} name={client.name} generated={generated}>
      <div id={client.rootId}>
        <Schema />
      </div>
    </SchemaUIProvider>
  );
};

function Schema() {
  const { refs, path, open, back } = useSchemaUI();
  const { schema } = path[path.length - 1];

  return (
    <>
      {path.length > 1 && <button onClick={() => back(path.length - 2)}>Back</button>}
      {schema.type === 'object' && (
        <ul>
          {schema.props.map((prop) => (
            <li key={prop.name}>
              {prop.name}:{' '}
              <button onClick={() => open(prop.name, prop.$type)}>
                {refs[prop.$type].typeName}
              </button>
            </li>
          ))}
        </ul>
      )}
    </>
  );
}
useSchemaUI()Is
paththe opened schemas, starting from the root
open(name, $ref), back(index)navigate path
getLink(index, name)the URL linking to a property of a path item, visiting it opens path again
highlightedthe property of the last path item that the URL links to

Each item of path has the schema to show, and its unions with the selected members. select() of a union shows another member.

The page passes renderMarkdown and renderCodeblock in, so descriptions and examples render through your own components. client carries the anchor ID and name of the root, links to a property are resolved against it.

Render the slots of client

Bodies and responses pass client.selector to select their media type, and client.actions to copy their TypeScript definitions.

Playground

usePlayground() holds the state of an API playground under <OperationProvider />. Its form starts from the selected example request, and edits sync back to the example, so code usages follow them.

components/my-playground.tsx
'use client';
import { StfProvider, useFieldValue } from '@fumari/stf';
import { useOperation } from 'fumadocs-openapi/operation';
import { usePlayground } from 'fumadocs-openapi/playground';

export function MyPlayground() {
  const { parameters } = useOperation();
  const { stf, send, isSending, response } = usePlayground();

  return (
    <StfProvider value={stf}>
      {parameters.map(({ in: location, items }) =>
        items.map((param) => (
          <ParameterInput key={`${location}.${param.name}`} location={location} name={param.name} />
        )),
      )}
      <button disabled={isSending} onClick={send}>
        Send
      </button>
      {response?.result.type === 'response' && <pre>{response.result.text()}</pre>}
    </StfProvider>
  );
}

function ParameterInput({ location, name }: { location: string; name: string }) {
  const [value, setValue] = useFieldValue([location, name]);

  return (
    <input
      aria-label={name}
      value={String(value ?? '')}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}
FieldIs
stfthe form (a @fumari/stf data engine), holding path, query, header, cookie and body
bodythe request body edited in the form, with its preferred media type
auththe auth fields of the selected security requirement, select() switches it, authorize() obtains an OAuth 2.0 token into a field
send(), isSending, response, clearResponse()send the form as a request, and the result of the last one

It accepts fetchOptions to customise requests, like onRequestInit(), authProviders to handle security schemes, see Authorization, and transformAuthInputs to customise the auth fields.

Requests

The parts usePlayground() builds on, to send a request without it.

ExportDoes
createBrowserFetcher()sends a request from the browser, through proxyUrl when set, its responses carry duration, mediaType, fileName and text()
encodeRequestData()turns the data of an example into a body, headers and query, from fumadocs-openapi/requests
resolveMediaAdapter()the adapter of a media type, undefined when unsupported
usePlaygroundAuth()the auth state of the page, its token store and the state of a flow
useAuthFields()the auth fields of an operation in a form, and the request data they encode into
requestOAuthToken()runs an OAuth 2.0 flow

How is this guide?

Last updated on