Skip to content

spec

spec() collects wired routes plus spec-level metadata, optional shared preds, and auth. Pass the result to mountSpec() to serve RPC, the docs UI, callspec.json, OpenAPI, and MCP.

typescript
spec({ routes, meta?, exports?, authenticate? })
OptionDefaultDescription
routesMap of wired routes — see Routes map.
meta{}Spec title, docs UI branding, OpenAPI info, MCP server hints — see Spec meta.
exportsNamed runtyp preds for frontend codegen — see Exports.
authenticateBearer hook — required when any route uses auth: 'bearer'. See Authentication.

Throws at load time if any route uses auth: 'bearer' and authenticate is missing.

Keys become RPC method namesroutes: { getProductById } is called as POST /v1/getProductById (plus your Express mount prefix). Values must come from route({ …, handler }), not bare preds.

By default only scope: 'public' routes appear in callspec.json, OpenAPI, SDK codegen, and MCP tools/list. Private routes still run on the server. Pass visibility: 'all' on mountSpec (or emitCallspec) to document them on that mount. See Auth and scope.

meta is flat JSON on the spec. It flows into emitted documents and into the docs UI when you mountSpec. It does not turn docs on or off — that is mountSpec(router, spec, { docs?, docsPath? }) (mountSpec options).

FieldDefaultUsed inDescription
title'Callspec API'Docs UI header, OpenAPI info.title, MCP server nameDisplay name for your API.
version'0.0.0'Docs UI header, OpenAPI info.version, MCP server versionSemver or build id — your choice. Always shown next to the API name.
introDocs UI home blurb, OpenAPI info.descriptionOptional welcome paragraph. Home is omitted when intro, website, and sdkInstall are all empty.
websiteDocs UI home link{ url, label? }label defaults to the hostname or “Learn more”.
logoCallspec hexDocs UI header + home{ light, dark? } — image URLs; omit to use the Callspec mark. See Logo URLs.
faviconlogo.lightDocs UI tab iconExplicit favicon URL; falls back to logo.light.
themeDocs UI CSS variables{ accent?, background?, surface?, fontFamily?, fontUrls? } — vars injected at boot. Accent-only keeps light/dark distinct; background / surface pin both modes and derive text for contrast.
navbarLinksDocs UI top header{ label, href, external? }[] — product links next to the brand.
footer{ poweredBy: true }Docs UI footer{ poweredBy?: boolean } — set poweredBy: false to hide “Powered by callspec”.
noticeDocs UI bannerPlain-text { title?, message, command?, links? } above the top header.
sdkInstallDocs UI homeStatic install hint with copy button (e.g. npm i @acme/sdk).
authHintautoDocs UI MCP connect panel (home page)Prose about Bearer tokens shown in the connect UI. Auto-set when bearer routes exist unless you override.
mcpInstructionsMCP server instructions fieldAgent-facing server description returned by MCP initialize — not shown in the docs UI connect panel.

Full whitelabel example (from the Chirp demo):

typescript
const meta = {
title: 'Chirp API v2',
version: '2.0.0',
intro: 'Read and write posts, timelines, and DMs.',
website: {url: 'https://chirp.social', label: 'chirp.social'},
logo: {
light: './brand/mark-light.png',
dark: './brand/mark-dark.png',
},
theme: {
accent: '#1d9bf0',
background: '#f7f9f9',
surface: '#ffffff',
},
navbarLinks: [
{label: 'chirp.social', href: 'https://chirp.social', external: true},
{label: 'GitHub', href: 'https://github.com/logfoxai/callspec', external: true},
],
authHint: 'Use Authorization: Bearer <token> for private routes.',
mcpInstructions: 'Chirp API — use Bearer demo in this sandbox.',
};
export const api = spec({meta, routes: {getUserById, …}});

Paths in logo.light / logo.dark are resolved relative to the docs UI URL (e.g. ./brand/mark.png under /v1/docs/v1/docs/brand/mark.png). Use absolute URLs (https://…) when the asset is hosted elsewhere.

Serve files under the docs path on the same router:

typescript
router.use('/docs/brand', express.static(path.join(__dirname, 'brand'), {index: false}));
mountSpec(router, api, {docsPath: '/docs'});
// meta.logo.light: './brand/mark.png' → /docs/brand/mark.png

If dark is omitted, the light logo is used in both themes.

mountSpec serves the built-in explorer by default at {mount}/docs. The UI loads {mount}/callspec.json, lists public routes, lets you try RPCs, browse schemas, and connect MCP clients.

What you wantWhere to configure
Turn docs/OpenAPI/callspec.json offmountSpec(…, {docs: false})
Change docs path onlymountSpec(…, {docsPath: '/explorer'}) — contract paths stay /callspec.json and /openapi.json
Title, intro, logo, theme, navbar, footerspec({ meta: { … } })
Per-route summaries and tagsroute({ meta: { summary, tags, … } })

More: Docs UI · Branding · mountSpec

Optional map of named runtyp preds that are not routes — shared form shapes, filters, enums for the frontend:

typescript
import {product, productList} from './schemas/product';
export const api = spec({
meta: {title: 'My API', version: '1.0.0'},
routes: {getProductById, listProducts},
exports: {product, productList},
});

exports land in callspec.json and appear on the generated schemas object (plus top-level Infer types):

bash
npx callspec http://127.0.0.1:3000/v1 --output src/generated/api.ts
typescript
import {schemas, type Product} from './generated/api';

See Shared validation and SDK generation.

typescript
import type {Authenticate} from 'callspec';
export type Ctx = {userId: string};
export const authenticate: Authenticate<Ctx> = async (token, req) => {
const session = await verifySession(token, req);
return session ? {userId: session.userId} : undefined;
};
export const api = spec({meta, routes, authenticate});

Callspec extracts Authorization: Bearer …, calls your hook, and passes the returned context to handlers on bearer routes. Return undefined for invalid tokens → 401. See Authentication and Request context.

route · Next: mountSpec