@routepact/core
v0.5.1
Published
Type-safe route spec definitions shared between server and client
Readme
@routepact/core
Define route pacts shared by your server and client.
Installation
npm install @routepact/coreAdd a Standard Schema library. Examples use Zod.
npm install zodDefining a pact
Pass named routes to definePact. Each route has a path, method, and schemas. HTTP routes require a response map; SSE routes require events instead. request and query are optional.
import { definePact } from "@routepact/core";
import { z } from "zod";
const post = z.object({ id: z.string(), title: z.string(), body: z.string() });
export const PostPacts = definePact({
list: {
method: "get",
path: "/posts",
response: {
200: z.object({
items: z.object({ id: z.string(), title: z.string() }).array(),
total: z.number(),
}),
},
query: z.object({
page: z.string().optional(),
limit: z.string().optional(),
}),
},
getById: {
method: "get",
path: "/posts/:id",
response: {
200: post,
404: z.object({ error: z.string() }),
},
},
create: {
method: "post",
path: "/posts",
request: z.object({ title: z.string(), body: z.string() }),
response: { 201: post },
},
update: {
method: "patch",
path: "/posts/:id",
request: z.object({ title: z.string().optional(), body: z.string().optional() }),
response: { 200: post },
},
delete: {
method: "delete",
path: "/posts/:id",
response: { 204: null }, // null = a status that carries no body
},
});Validation schemas
| Field | Required for | Description |
| ---------- | ------------------------------- | ------------------------------------------------------------------------------------------- |
| response | Every non-SSE route | Map of HTTP status code → object schema (or null for a bodyless status) |
| events | Every SSE route | Object schema for the SSE event shape — replaces response on sse: true routes |
| request | Only for post, patch, put | Object schema for the request body on the server |
| query | Optional | Object schema for query parameters — typed on both server and client |
TypeScript rejects missing or conflicting response and events fields. Schemas must describe objects, such as z.object(...).
Response statuses
response is keyed by HTTP status code, so each status gets its own validated body:
response: {
200: z.object({ id: z.string(), name: z.string() }),
404: z.object({ error: z.string() }),
204: null, // declared, but carries no body
}Server and client validate the body against the returned status. For a null status, return only { status: 204 }; the response has no body.
Narrow on status to get its body type:
// handler
route.handler(({ params }) =>
found
? { status: 200, body: { id: params.id, name: "Alice" } }
: { status: 404, body: { error: "not found" } },
);Server-Sent Events (SSE)
SSE routes use sse: true and an events schema. The handler sends validated events with sendEvent. Use a discriminated union for multiple event shapes:
export const NotificationPacts = definePact({
stream: {
method: "get",
path: "/notifications/:userId",
sse: true,
events: z.discriminatedUnion("type", [
z.object({ type: z.literal("message"), text: z.string() }),
z.object({ type: z.literal("ping"), timestamp: z.number() }),
]),
},
});SSE handlers return void. See the adapter READMEs for handler and connection examples.
SSE routes support get and post. Use post with a request schema when the subscription needs a body. A get stream can also be consumed with browser EventSource.
Path parameters
Path segments such as :id become typed string parameters. No schema is needed.
// TypeScript requires params when the path has parameters
await client.request(PostPacts.getById, { params: { id: "abc" } });
// TypeScript forbids params when there are none
await client.request(PostPacts.list);For /posts/:id, server ctx.params has type { id: string }.
Query parameters
Add a query schema to type query parameters. Required schema fields make the client's query option required:
const list = {
method: "get",
path: "/posts",
query: z.object({ page: z.string().optional(), sort: z.string() }), // sort is required
};
// TypeScript requires query.sort
await request(PostPacts.list, { query: { sort: "createdAt" } });Undeclared request data
Handlers and middleware receive only declared request data:
- A route with no
queryschema getsctx.query === {}, even if the client sent query parameters. - A route with a
queryschema gets only the keys that schema declares — extras are stripped by validation. - A route with no
requestschema getsctx.body === undefined, even if the client sent a payload.
Path parameters come from the route path and are percent-decoded strings. For raw input, use ctx.req (Express) or ctx.c (Hono).
Validation errors
| Error class | Status | When |
| ------------------------- | ------ | --------------------------------------------- |
| RequestValidationError | 400 | Request body or query fails schema validation |
| ResponseValidationError | 500 | Response body fails schema validation |
Both extend ValidationError and expose a cause property with the Standard Schema issues array.
