Skip to content
Blume
Esc
↑↓navigate↵open⌘Jpreview
Guides

API reference

Generate API docs from an OpenAPI spec

Turn an OpenAPI file into a docs site with a page per operation, a request playground, and hand-written guides beside the reference.

By 8 min read

By the end of this guide, you have a docs site built from an OpenAPI spec. It has an overview page for the API, one page per operation with its parameters, schemas, and code samples, a Try it panel that sends real requests, and a hand-written authentication page in the same sidebar. Every page is static HTML you can host anywhere, with its own URL, search entry, and Markdown copy for agents.

The example is a fictional Acme Messages API that sends email and SMS. Swap in your own spec as you go: every step works the same way.

Create the project

Scaffold a new Blume project:

npx blume init acme-docs

Pick the docs template when it asks, and keep the default docs content folder. You get a docs/index.mdx home page, a blume.config.ts, and a package.json with dev and build scripts. There's also an api template that starts you on a sample pet store spec at /api, but starting from docs shows each piece as you add it.

Add your spec

Save the spec as openapi.yaml at the project root, beside blume.config.ts. A local file is the safer choice: Blume reads it at build time, so a build never depends on fetching the spec from somewhere else. JSON works too, and so do Swagger 2.0 and OpenAPI 3.0 specs, which Blume upgrades to 3.1 as it reads them.

openapi: 3.1.0
info:
  title: Acme Messages API
  version: 1.0.0
  description: Send transactional email and SMS, and manage the templates they use.
servers:
  - url: https://api.acme.example/v1
security:
  - bearerAuth: []
tags:
  - name: Messages
    description: Send a message and check whether it arrived.
  - name: Templates
    description: Reusable message bodies with variables.
paths:
  /messages:
    post:
      operationId: sendMessage
      tags: [Messages]
      summary: Send a message
      description: Queues an email or SMS for delivery and returns its ID right away.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendMessageRequest"
            example:
              channel: email
              to: ada@example.com
              template: welcome
      responses:
        "202":
          description: The message is queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
  /messages/{id}:
    get:
      operationId: getMessage
      tags: [Messages]
      summary: Get a message
      description: Returns a message and its delivery status.
      parameters:
        - name: id
          in: path
          required: true
          description: The message ID, from the response to Send a message.
          schema:
            type: string
          example: msg_8f2k
      responses:
        "200":
          description: The message.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "404":
          description: No message has that ID.
  /templates:
    get:
      operationId: listTemplates
      tags: [Templates]
      summary: List templates
      description: Returns every template in the workspace.
      responses:
        "200":
          description: The templates.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Template"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An API key, sent as a bearer token.
  schemas:
    SendMessageRequest:
      type: object
      required: [channel, to, template]
      properties:
        channel:
          type: string
          enum: [email, sms]
          description: How to deliver the message.
        to:
          type: string
          description: An email address, or a phone number in E.164 format.
        template:
          type: string
          description: The ID of the template to send.
        variables:
          type: object
          additionalProperties:
            type: string
          description: Values for the template's variables.
    Message:
      type: object
      properties:
        id:
          type: string
          example: msg_8f2k
        status:
          type: string
          enum: [queued, sent, delivered, failed]
        channel:
          type: string
          enum: [email, sms]
    Template:
      type: object
      properties:
        id:
          type: string
          example: welcome
        name:
          type: string
          example: Welcome email

The api.acme.example server doesn't exist. When you use your own spec, its servers list is where the Try it panel sends requests, so point it at your real API.

Mount the reference

Import the openapi() adapter from blume/reference and list it under reference. Then add header tabs, one for your guides and one for the reference, so readers can reach it:

import { defineConfig } from "blume";
import { openapi } from "blume/reference";

export default defineConfig({
  title: "Acme Docs",
  navigation: {
    tabs: [
      { label: "Docs", path: "/" },
      { label: "API reference", path: "/api" },
    ],
  },
  reference: [openapi({ route: "/api", spec: "./openapi.yaml" })],
});

route sets where the reference lives. Without it, the reference mounts at /reference. The reference never adds a header tab on its own, so the tab pointing at the same route is what makes it reachable, and it also scopes the sidebar to the reference's pages.

Start the dev server and open the site:

npm run dev

The API reference tab opens an overview at /api with the API's version, its server URL, and a section per tag listing its operations. Each operation has its own page, at a URL built from its tag and operation ID:

OperationPage
POST /messages/api/messages/send-message
GET /messages/{id}/api/messages/get-message
GET /templates/api/templates/list-templates

A camelCase operation ID like sendMessage is split into send-message. An operation with no ID takes its method and path instead, so give every operation an ID if you want URLs that read well.

Make the spec read well

Blume builds every page from what the spec says, so most improvements to your reference are improvements to the spec. Here's where each field shows up:

In the specOn the site
info.title, info.descriptionThe overview page's title and introduction
A tag's descriptionThe paragraph under that tag's section on the overview
An operation's summaryThe page title and sidebar label. Without one, the page is titled POST /messages.
An operation's descriptionThe page's introduction and its search result description
Parameter and schema descriptionsThe parameter and schema tables
example valuesThe examples, and the values the Try it form starts with
securityAn Authorization section, and a placeholder credential in every code sample

Because the example spec sets security at the root, every operation shows an Authorization section, and its curl sample sends Authorization: Bearer YOUR_TOKEN. To mark one operation as public, give it security: [].

Add an authentication guide

A reference answers "what does this endpoint take?" Readers also need "how do I make my first call?", and that belongs in a page you write. Put it in a folder named after the reference route, and it joins the API reference tab's sidebar beside the generated pages:

---
title: Authentication
description: Send an API key as a bearer token with every request to the Acme Messages API.
---

Every request needs an API key, sent in the `Authorization` header as a
bearer token.

## Send your first message

```bash
curl https://api.acme.example/v1/messages \
  -H "Authorization: Bearer $ACME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel":"email","to":"ada@example.com","template":"welcome"}'
```

The response holds the message ID. Pass it to
[Get a message](/api/messages/get-message) to check whether it arrived.

The page lives at /api/authentication, and it links to an operation page by its URL like any other page. That's the whole trick for mixing guides and reference: they're all pages, so they share the sidebar, search, and links.

Try a request

Every operation page has a Try it panel, collapsed until a reader opens it. Its form comes from the operation: a field per parameter, a body editor built from the request schema, and an input for the bearer token. The code samples beside it update as the form changes, so a copied curl command always matches what Send would do.

Credentials typed into the panel stay in memory and are gone on reload, unless the reader checks Remember on this device. Code samples keep showing YOUR_TOKEN unless the reader turns on Include my values in samples, so a screenshot or copied snippet doesn't leak a key.

Why Try it fails in the browser

The panel sends requests straight from the reader's browser to your API. Browsers only allow that when the API answers with CORS headers that permit the docs site's origin. If your API doesn't, Send fails even though the same request works from curl.

You have two ways out. Add your docs origin to the API's CORS allowlist, or route requests through a proxy. Blume ships one: set playground.proxy to true. It runs as a server route, so it needs a host adapter from blume/deploy:

import { defineConfig } from "blume";
import { vercel } from "blume/deploy";
import { openapi } from "blume/reference";

export default defineConfig({
  deployment: vercel(),
  reference: [
    openapi({
      playground: { proxy: true },
      route: "/api",
      spec: "./openapi.yaml",
    }),
  ],
});

The proxy only forwards requests to the servers your spec declares, and only the headers the panel sets, so it can't be aimed at other hosts. If your API already allows the docs origin, skip it and keep a static build.

Choose your code samples

Each operation shows samples in curl, JavaScript, and Python by default. Pick your own languages, in the order you want them:

openapi({
  codeSamples: ["curl", "typescript", "python", "go"],
  route: "/api",
  spec: "./openapi.yaml",
}),

If you publish an SDK, show it instead of raw HTTP: add x-codeSamples to an operation in the spec, each with a lang, a source, and an optional label. Those tabs come first, ahead of the generated ones.

Build and check

Build the site and serve the production build locally:

npm run build
npx blume preview

Open the URL it prints and check three things:

  • The operation URL. /api/messages/send-message loads on its own, not only from the sidebar.
  • Search. Searching for "send a message" or POST /messages finds the operation.
  • The Markdown copy. /api/messages/send-message.md returns the page as Markdown, with the endpoint written out as POST /messages. That's what agents read, and it's listed in your site's llms.txt too.

Search matches an operation's summary, description, tag, and endpoint. It doesn't index every field in the schema tables or the code samples, so if readers search for a field name, mention it in the operation's description.

Run npx blume validate as well. It checks every internal link, including the links from your guides to operation pages.

Deploy

Without the proxy, the whole site is static files in dist/, so any static host works. On Vercel and Netlify, Blume detects the site's production URL. Cloudflare Pages only exposes each deploy's own URL, which changes every time, and other hosts expose none, so there, set deployment.site to your docs URL so the sitemap, canonical links, and Open Graph images are correct. For a step-by-step static deploy, see Deploy Markdown docs to GitHub Pages. With the proxy on, deploy to the host your adapter names; the deployment docs cover each one.

When the spec changes

The reference is rebuilt from the spec on every build, so publishing an updated spec is a matter of committing it and deploying. The dev server watches your content folder and blume.config.ts, not a spec file beside them, so restart it after you edit the spec.

One change needs care. An operation's URL comes from its tag and operation ID, so renaming either one moves the page. Add a redirect for the old URL so links and search results keep working:

redirects: [
  { from: "/api/messages/send-message", to: "/api/messages/create-message" },
],

If the spec comes from your code or another team, and you want to trim or reword it for the docs without editing it, remove internal endpoints with an overlay instead.

Troubleshooting

The build fails with BLUME_OPENAPI_UNAVAILABLE

Blume couldn't read or parse the spec. The message says why: a wrong path, a failed download, or invalid YAML. In blume dev it's a warning and the reference is skipped, so a working dev server can still hide it. Check the path is relative to the project root, like ./openapi.yaml.

The reference is empty

BLUME_OPENAPI_EMPTY means the file parsed but declares no operations under paths. Check that spec points at the OpenAPI document itself, not another YAML file.

There's no API tab

The reference doesn't add a tab. Add one to navigation.tabs whose path matches the adapter's route exactly.

Send fails but curl works

That's CORS. Allow the docs origin in your API, or turn on the proxy as shown above. With the proxy on, a Custom base URL typed into the panel is refused with a 403, because it isn't one of the spec's servers. Fix CORS errors in an API documentation playground walks through reading the failed preflight.

A page's URL changed

You renamed an operation ID or moved an operation to another tag. Add a redirect from the old URL, and run npx blume validate to find internal links that still point at it.

Next step

Start your API docs

Start a project, drop your spec beside it, and add the reference to your config.

npx blume init
Read the OpenAPI reference docs

A step here not working for you? Report a broken step.

Keep going.More guides.

Upgrade your docs with Blume.

Install today and ship a production-grade docs site in minutes. Free and open source, forever.

npx blume init