Kynetra FX

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.

src/openapi.ts
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.

NameTypeDescription
titlestringAPI title placed in the info object. Required.
versionstringAPI version string placed in the info object. E.g. '1.0.0'. Required.
descriptionstring | undefinedOptional 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.

src/serve-spec.ts
import { createApp, generateOpenAPI } from '@kynetra/fx'
 
const app = createApp()
 
// ... register your routes ...
 
// Serve the raw JSON spec
app.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.

src/swagger-ui.ts
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 :id in the route definition become OpenAPI parameters with in: 'path'.
  • Request body — the input schema on a contract becomes the requestBody for POST, PUT, and PATCH operations, with application/json content type.
  • Query parameters — the query schema is expanded into individual parameters with in: 'query'.
  • Response body — the output schema maps to the 200 response content under application/json.
  • 422 validation error — any route that has an input or query schema automatically gets a 422 response entry describing the FX_VALIDATION_ERROR shape.
  • Operation metadataoperationId, tags, summary, and description are taken from the contract config when provided.

Note

Only routes registered with 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.

src/operation-metadata.ts
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.

src/schema-to-json.ts
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.

src/third-party-schemas.ts
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 spec
const 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.

src/scalar.ts
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.

src/redoc.ts
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

For production deployments, consider generating the spec at build time and serving it as a static file. This avoids the small overhead of re-generating on every request and lets you cache the file at the edge.