Documentation
OpenAPI
Kynetra FX derives an OpenAPI 3.1 specification directly from your contract routes. No decorators, no separate schema files — the same definitions that drive validation also drive the spec. Register a route once and get accurate documentation automatically.
Generating the spec#
generateOpenAPI takes your app instance and a metadata object. It returns a plain JavaScript object representing an OpenAPI 3.1 document — you can serialise it, cache it, serve it, or pipe it into any tooling that understands the format.
import { createApp, generateOpenAPI } from '@kynetra/fx' const app = createApp() app.route('GET /users', { query: UserListQuerySchema, output: UserListSchema,}) app.route('POST /users', { input: CreateUserSchema, output: UserSchema,}) const spec = generateOpenAPI(app, { title: 'Example API', version: '1.0.0', description: 'Public API for the Example platform.',}) // spec is { openapi: '3.1.0', info: { ... }, paths: { ... } }console.log(spec.openapi) // '3.1.0'console.log(Object.keys(spec.paths)) // ['/users']The title and version fields are required. description is optional and appears in the info object of the generated document.
| Name | Type | Description |
|---|---|---|
| title | string | API title placed in the info object. Required. |
| version | string | API version string placed in the info object. E.g. '1.0.0'. Required. |
| description | string | undefined | Optional longer description of the API placed in the info object. |
Serving the spec#
The most common pattern is to add a /openapi.json route that generates and returns the spec on demand. Because generateOpenAPI is just a function call, you can call it inside any handler.
import { createApp, generateOpenAPI } from '@kynetra/fx' const app = createApp() // ... register your routes ... // Serve the raw JSON specapp.get('/openapi.json', (ctx) => { const spec = generateOpenAPI(app, { title: 'Example API', version: '1.0.0', }) return ctx.json(spec)})You can also serve a Swagger UI HTML page that loads the spec from that endpoint. The snippet below uses the official Swagger UI CDN and requires no npm install.
app.get('/docs', (ctx) => { const html = `<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8" /> <title>API Docs</title> <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" /></head><body> <div id="swagger-ui"></div> <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script> <script> SwaggerUIBundle({ url: '/openapi.json', dom_id: '#swagger-ui', presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset], }) </script></body></html>` return ctx.html(html)})What gets emitted#
The generator reads every contract registered via app.route() and maps its fields to OpenAPI constructs. The mapping is consistent and predictable.
- Paths — each
app.route()call contributes a path entry. The HTTP method and path are parsed from the first string argument. - Path parameters — segments like
:idin the route definition become OpenAPIparameterswithin: 'path'. - Request body — the
inputschema on a contract becomes therequestBodyfor POST, PUT, and PATCH operations, withapplication/jsoncontent type. - Query parameters — the
queryschema is expanded into individualparameterswithin: 'query'. - Response body — the
outputschema maps to the 200 response content underapplication/json. - 422 validation error — any route that has an
inputorqueryschema automatically gets a 422 response entry describing theFX_VALIDATION_ERRORshape. - Operation metadata —
operationId,tags,summary, anddescriptionare taken from the contract config when provided.
Note
app.route() appear in the spec. Plain app.get / app.post handlers are not included.Customizing operations#
Add operationId, tags, summary, and description to any route contract to enrich the generated operation object. These fields are passed through verbatim into the OpenAPI output.
import { createApp, generateOpenAPI } from '@kynetra/fx' const app = createApp() app.route('GET /users', { operationId: 'listUsers', tags: ['Users'], summary: 'List all users', description: 'Returns a paginated list of users filtered by the supplied query parameters.', query: UserListQuerySchema, output: UserListSchema,}) app.route('POST /users', { operationId: 'createUser', tags: ['Users'], summary: 'Create a user', description: 'Creates a new user and returns the created resource.', input: CreateUserSchema, output: UserSchema,}) app.route('GET /orgs/:orgId/members', { operationId: 'listOrgMembers', tags: ['Organizations', 'Users'], summary: 'List organisation members', output: MemberListSchema,}) const spec = generateOpenAPI(app, { title: 'Example API', version: '1.0.0' })Tags are a useful way to group operations in rendered documentation. Scalar, Swagger UI, and Redoc all display operations grouped by tag in the sidebar by default. Operations without a tag appear in a default group.
schemaToJSON#
schemaToJSON converts a Kynetra FX schema into a standalone JSON Schema object. This is useful when you need the JSON Schema representation outside of the OpenAPI context — for example to embed in a non-OpenAPI validator, store in a database, or send to a frontend for client-side validation.
import { s, schemaToJSON } from '@kynetra/fx' const UserSchema = s.object({ id: s.number(), name: s.string(), email: s.string(), role: s.enum(['admin', 'member', 'viewer']),}) const jsonSchema = schemaToJSON(UserSchema) // {// type: 'object',// properties: {// id: { type: 'number' },// name: { type: 'string' },// email: { type: 'string' },// role: { enum: ['admin', 'member', 'viewer'] },// },// required: ['id', 'name', 'email', 'role'],// }console.log(jsonSchema)See Schemas for the full list of schema primitives and combinators available in the built-in schema builder.
Third-party schemas#
Standard Schema validators — including Zod and Valibot — can be used as the input, query, and output on any contract. When you do, their JSON Schema representation is extracted via the Standard Schema ~standard protocol and included in the OpenAPI output, exactly as it would be for a native FX schema.
import { createApp, generateOpenAPI } from '@kynetra/fx'import { z } from 'zod' const app = createApp() const CreateUserInput = z.object({ name: z.string().min(1), email: z.string().email(), role: z.enum(['admin', 'member']).default('member'),}) const UserOutput = z.object({ id: z.number(), name: z.string(), email: z.string(), role: z.string(), createdAt: z.string(),}) app.route('POST /users', { operationId: 'createUser', tags: ['Users'], input: CreateUserInput, // Zod schema — extracted via ~standard output: UserOutput, // Zod schema — extracted via ~standard}) // Zod's JSON Schema is correctly reflected in the generated specconst spec = generateOpenAPI(app, { title: 'Example API', version: '1.0.0' })See Standard Schema for the full list of supported validators and how the extraction protocol works.
Integrating with tooling#
The generated spec is a plain JSON object conforming to OpenAPI 3.1 and can be fed directly into any compatible tool. Popular options include Scalar, Swagger UI, Redoc, and Postman.
Scalar
Scalar produces a polished, modern API reference UI. Use the CDN snippet to serve it from your app with zero dependencies.
app.get('/reference', (ctx) => { const html = `<!doctype html><html><head> <title>API Reference</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /></head><body> <script id="api-reference" data-url="/openapi.json" src="https://cdn.jsdelivr.net/npm/@scalar/api-reference" ></script></body></html>` return ctx.html(html)})Postman
Postman can import an OpenAPI 3.1 JSON file directly from a URL. Point it at your /openapi.json endpoint to generate a complete collection with all routes, parameters, and example bodies pre-filled from the schema.
Redoc
Redoc renders a three-panel API reference with a sidebar, main content, and code samples. Like Scalar, it can be embedded in a single HTML page that loads the spec from your endpoint.
app.get('/redoc', (ctx) => { const html = `<!DOCTYPE html><html><head> <title>API Docs</title> <meta charset="utf-8" /></head><body> <redoc spec-url='/openapi.json'></redoc> <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script></body></html>` return ctx.html(html)})Tip