Skip to content

Getting started

This page walks through a simple server and the client.

For coding agents: Working with Coding Agents (copy-paste prompts; skill SKILL.md).

bash
npm i callspec runtyp express
npm i -D tsx typescript @types/express

Requirements: Node.js 18+ (runtime), TypeScript 5+, Express 4.x (peer), runtyp 2.5.0 (peer).

src/routes/getProductById.ts
import {route, err} from 'callspec';
import {predicates as p, Infer} from 'runtyp';
const product = p.object({
id: p.string(),
name: p.string(),
priceCents: p.number(),
});
type Product = Infer<typeof product>;
const products: Product[] = [
{id: 'sku-1', name: 'Widget', priceCents: 999},
{id: 'sku-2', name: 'Gadget', priceCents: 1299},
];
export const getProductById = route({
input: p.object({id: p.string()}),
output: product,
meta: {summary: 'Get product by ID', tags: ['catalog']},
auth: 'none',
mcp: true,
// Keep the handler inline for LSP support
handler: async (input, _ctx) => {
const found = products.find((item) => item.id === input.id);
if (!found) return err.NOT_FOUND();
return found;
},
});

Callspec validates input before your handler runs. Learn more about routes

src/spec.ts
import {spec} from 'callspec';
import {getProductById} from './routes/getProductById';
export const api = spec({
meta: {title: 'My API', version: '1.0.0'},
routes: {getProductById},
});

Related: spec · Server layout

src/index.ts
import {mountSpec} from 'callspec';
import express from 'express';
import {api} from './spec';
const app = express();
const router = express.Router();
mountSpec(router, api); // parses application/json on this router
app.use('/v1', router);
const port = 3000;
app.listen(port, () => {
console.log(`RPC: http://127.0.0.1:${port}/v1/getProductById`);
console.log(`Docs: http://127.0.0.1:${port}/v1/docs`);
console.log(`Callspec: http://127.0.0.1:${port}/v1/callspec.json`);
console.log(`OpenAPI: http://127.0.0.1:${port}/v1/openapi.json`);
console.log(`MCP: http://127.0.0.1:${port}/v1/mcp`);
});
bash
npx tsx src/index.ts

Open http://127.0.0.1:3000/v1/docs.

Related: Docs UI · mountSpec · MCP Server · OpenAPI

bash
# Live mount (server running) — pass the mount point
npx callspec http://127.0.0.1:3000/v1 --output src/generated/api.ts
# Optional — pin the contract for CI / offline codegen
curl -fsS http://127.0.0.1:3000/v1/callspec.json -o callspec.json
npx callspec ./callspec.json --output src/generated/api.ts

Related: SDK generation · Shared validation (schemas from codegen)

Each method returns a Result — check result.ok, handle the codes that matter for that UI, and send the rest through a shared helper (you do not need a giant switch at every call site).

src/app/getProductById.ts
import {ApiClient} from '../generated/api';
const api = new ApiClient({
baseUrl: 'http://127.0.0.1:3000/v1',
});
export async function fetchProduct(id: string) {
const result = await api.getProductById({id});
if (!result.ok) {
if (result.code === 'NOT_FOUND') {
console.error(`Unknown sku ${id}`);
return null;
}
console.error(result.code, result.data);
return null;
}
return result.value; // { id, name, priceCents }
}
const product = await fetchProduct('sku-1');
console.log(product?.name, product?.priceCents);

Shared helper + optional exhaustive switch: Client usage.

Related: Client usage · Builtin errors · Authentication