Fumadocs

OpenAPI

Generating docs for OpenAPI schema.

Setup

Install the required packages.

npm i fumadocs-openapi shiki

Generate Styles

Add the following line:

Tailwind CSS
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-openapi/css/preset.css';

Configure Plugin

Create the OpenAPI server instance & <OpenAPIPage /> component.

import { createOpenAPI } from 'fumadocs-openapi/server';

// note: this is a server-side API
export const openapi = createOpenAPI({
  // the OpenAPI schema, you can also give it an external URL.
  input: ['./openapi.json'],
});

See createOpenAPI() & createOpenAPIPage() for available options.

Generate Pages

You can generate MDX files directly from your OpenAPI schema.

Create a script:

scripts/generate-docs.ts
import { generateFiles } from 'fumadocs-openapi';
import { openapi } from '@/lib/openapi';

void generateFiles({
  input: openapi,
  output: './content/docs',
  // we recommend to enable it
  // make sure your endpoint description doesn't break MDX syntax.
  includeDescription: true,
});

Generate docs with the script:

bun ./scripts/generate-docs.ts

Add the OpenAPIPage component to your MDX components.

app/docs/[[...slug]]/page.tsx
import { source } from '@/lib/source';
import { openapi } from '@/lib/openapi';
import { OpenAPIPage } from '@/components/api-page';
import { getMDXComponents } from '@/components/mdx';

// e.g. in your page renderer
export default function Page({ slug }) {
  const page = source.getPage(slug);
  const MdxContent = page.data.body;

  return (
    <MdxContent
      components={getMDXComponents({
        // add the MDX component
        OpenAPIPage: async (props) => (
          <OpenAPIPage {...await openapi.preloadOpenAPIPage(page)} {...props} />
        ),
      })}
    />
  );
}

You can also use it without generating real files by integrating into Loader API.

lib/source.ts
import { loader } from 'fumadocs-core/source';
import { docs } from 'collections/server';
import { openapi } from '@/lib/openapi';

export const source = loader(
  {
    docs: docs.toFumadocsSource(),
    openapi: await openapi.staticSource({
      baseDir: 'openapi',
    }),
  },
  {
    baseUrl: '/docs',
    plugins: [openapi.loaderPlugin()],
    // ...
  },
);

staticSource() is a server-side API that generates pages directly to your loader(), hence it allows dynamic content generation, such as re-generating page tree as schema changes.

It will change the type of your pages, make sure to update all references to your source.

For example, where you return text for LLM:

import { source } from '@/lib/source';

export async function getLLMText(page: (typeof source)['$inferPage']) {
  if (page.type === 'openapi') {
    // e.g. return the stringified OpenAPI schema
    return JSON.stringify(page.data.getSchema().bundled, null, 2);
  }

  // your original flow below...
}

And update your page renderer:

docs/[[...slug]]/page.tsx
import { OpenAPIPage } from '@/components/api-page';

export default function Page({ slug }) {
  const page = source.getPage(slug);

  // for OpenAPI pages
  if (page.type === 'openapi') {
    return (
      <DocsPage full>
        <h1 className="text-[1.75em] font-semibold">{page.data.title}</h1>
        <DocsBody>
          <OpenAPIPage {...page.data.getOpenAPIPageProps()} />
        </DocsBody>
      </DocsPage>
    );
  }

  // your original flow below...
}

Pass a client payload from server, then render the page using the <OpenAPIPage /> component you created above.

For example, in Tanstack Start:

routes/docs/$.tsx
import { createFileRoute, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { createServerFn } from '@tanstack/react-start';
import { source } from '@/lib/source';
import browserCollections from 'collections/browser';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { type ReactNode, Suspense } from 'react';
import { OpenAPIPage } from '@/components/api-page';

export const Route = createFileRoute('/docs/$')({
  component: Page,
  loader: async ({ params }) => {
    const slugs = params._splat?.split('/') ?? [];
    const data = await serverLoader({ data: slugs });

    // Fumadocs MDX: only preload content for normal pages
    if (data.type === 'docs') {
      await clientLoader.preload(data.path);
    }
    return data;
  },
});

const serverLoader = createServerFn({
  method: 'GET',
})
  .inputValidator((slugs: string[]) => slugs)
  .handler(async ({ data: slugs }) => {
    const page = source.getPage(slugs);
    if (!page) throw notFound();

    const pageTree = await source.serializePageTree(source.getPageTree());
    // different result for OpenAPI pages
    if (page.type === 'openapi') {
      return {
        type: 'openapi',
        title: page.data.title,
        description: page.data.description,
        pageTree,
        props: page.data.getOpenAPIPageProps(),
      };
    }

    return {
      type: 'docs',
      path: page.path,
      pageTree,
      // ...
    };
  });

const clientLoader = browserCollections.docs.createClientLoader({
  component(pageData, props) {
    // ...
  },
});

function Page() {
  const page = useFumadocsLoader(Route.useLoaderData());
  let content: ReactNode;

  // render OpenAPI page content
  if (page.type === 'openapi') {
    content = (
      <DocsPage full>
        <DocsTitle>{page.title}</DocsTitle>
        <DocsDescription>{page.description}</DocsDescription>
        <DocsBody>
          {/* pass the payload data */}
          <OpenAPIPage {...page.props} />
        </DocsBody>
      </DocsPage>
    );
  } else {
    content = clientLoader.useContent(page.path, page);
  }

  return (
    <DocsLayout tree={page.pageTree}>
      <Suspense>{content}</Suspense>
    </DocsLayout>
  );
}

You can see the full Tanstack Start example.

Ensure the migration is complete!

Run a type check to verify before continuing, e.g.

npm run types:check

Features

The official OpenAPI integration supports:

  • Basic API endpoint information
  • Interactive API playground
  • Example code to send request (in different programming languages)
  • Response samples and TypeScript definitions
  • Request parameters and body generated from schemas

Demo

View demo.

How is this guide?

Last updated on

On this page