FrançaisPlayground

OpenAPI

The route contracts already say what each route accepts, returns and requires, so the API description is generated from them and cannot drift from what the server does. Postman, Insomnia, Bruno and code generators import it.

// src/api/openapi.json.ts
import { apiRoutes } from '@fluixi/start/api-routes';
import { openApiRoute } from '@fluixi/api';

export const GET = openApiRoute(() => apiRoutes, {
  title: 'Acme API',
  version: '1.0.0',
  servers: [{ url: 'https://api.acme.com' }],
});

GET /api/openapi.json now serves an OpenAPI 3.1 document. apiRoutes is the app's route table, with path templates ([id].ts becomes {id}, a catch-all becomes its parameter), and is read per request, since this route is in it too. The route serving the document leaves itself out.

What each operation says

From the contract In the document
params, query, body, response parameters, request body and response schema
status the success response's code
guard security, the scheme under its name, and the 401 and 403 responses it can give
a route without a guard security: [], so a tool does not send credentials it does not need
input schemas a 400 response, with the error shape
summary, description, tags, operationId, deprecated as given

Without them, the tag is the first meaningful path segment (/api/v1/projects/{id} is in projects), the summary is the method and path, and the operation id is built from both (getProjectsById).

A plain handler, one not made with route(), still appears, marked x-fluixi-contract: false, so the gap is visible rather than silent.

Schemas

JSON Schema comes from the validator itself, through the Standard JSON Schema interface, which Zod 4 implements. For a validator that does not, give a converter once:

import { setJsonSchemaConverter } from '@fluixi/api';
import { toJsonSchema } from '@valibot/to-json-schema';

setJsonSchemaConverter((schema) => toJsonSchema(schema as never));

A schema nothing can convert is described as any value and marked x-fluixi-unconverted with the validator's name, so the document still builds and the gap shows.

Routes a library handles

An auth engine or a webhook receiver often serves many paths from one catch-all route, and describes them in its own OpenAPI document. describedBy puts that document in place of the route:

// src/api/auth/[...all].ts
import { describedBy } from '@fluixi/api';

export const GET = describedBy(
  () => auth.api.generateOpenAPISchema(),
  (request: Request) => auth.handler(request),
);
export const POST = GET;

The library's paths appear under the route's path (/api/auth/sign-in/email and the rest), with its tags and components. Where a component or a security scheme has the same name as one of yours but a different shape, the library's is renamed after the mount (AuthUser), and so is a clashing operation id (authGetOrganization); references follow. OpenAPI 3.0's nullable is written the 3.1 way, and an operation without a summary gets one, since tools name requests after it.

includeDocument(target, source, mount) does the same for a document you build yourself.

Importing into Postman and friends

Point the tool at /api/openapi.json. Each operation becomes a request, grouped by tag, with its parameters and an example body. For an API that signs in with a session cookie, call the sign-in route once and let the tool's cookie jar carry the cookie: some importers turn a cookie scheme into a request header, which then needs removing from the requests.