@routepact/client
v0.1.13
Published
Ky-based type-safe HTTP client for route pacts - validates responses against Zod schemas
Downloads
1,473
Readme
@routepact/client
ky-based HTTP client for @routepact/core pacts. Pass a pact and get back a fully-typed response - params, payload, queries, and return type are all inferred automatically.
Installation
npm install @routepact/client @routepact/core kyYou also need a schema library that implements the Standard Schema interface (e.g. Zod, Valibot, ArkType) for defining your pacts. Examples below use Zod but any Standard Schema-compatible library works.
npm install zod # or valibot, arktype, etc.Setup
Create a request function by binding a ky instance and a base URL. You typically do this once and export it for use across your app.
import ky from "ky";
import { createRequest } from "@routepact/client";
const api = ky.create({
headers: { "Content-Type": "application/json" },
credentials: "include",
});
export const request = createRequest(api, "https://api.example.com");Making requests
Pass a pact to request. TypeScript infers everything from the pact - what options are required, what the return type is, and whether params, payload, or query are needed.
import { PostPacts } from "../shared/pacts/post.pact";
// GET /posts - no options needed
const posts = await request(PostPacts.list);
// posts: { id: string; title: string }[]
// GET /posts/:id - params are required
const post = await request(PostPacts.getById, {
params: { id: "abc" },
});
// post: { id: string; title: string; body: string }
// POST /posts - payload is required, typed from the request schema
const created = await request(PostPacts.create, {
payload: { title: "Hello", body: "World" },
});
// created: { id: string; title: string; body: string }
// PATCH /posts/:id - both params and payload
const updated = await request(PostPacts.update, {
params: { id: "abc" },
payload: { title: "Updated title" },
});
// DELETE /posts/:id - params required, no response body
await request(PostPacts.delete, { params: { id: "abc" } });Query parameters
Query parameters are typed from the pact's query schema. If the schema has required fields, TypeScript will require query at the call site:
// pact defined with: query: z.object({ page: z.string().optional(), sort: z.string() })
const posts = await request(PostPacts.list, {
query: { sort: "createdAt", page: "2" },
});
// → GET /posts?sort=createdAt&page=2If the pact has no query schema, query accepts never and TypeScript will prevent you from passing it.
Customising the ky instance
Since createRequest accepts any KyInstance, you can configure ky however you like before passing it in - hooks, auth headers, retry logic, etc.
import ky from "ky";
import { createRequest } from "@routepact/client";
const api = ky.create({
prefixUrl: "https://api.example.com",
retry: { limit: 2 },
hooks: {
beforeRequest: [
(request) => {
request.headers.set("Authorization", `Bearer ${getToken()}`);
},
],
},
});
export const request = createRequest(api, "");
// baseUrl is empty because prefixUrl is set on the ky instanceYou can also pass per-request ky hooks via the hooks option:
const post = await request(PostPacts.getById, {
params: { id: "abc" },
hooks: {
beforeRequest: [(req) => req.headers.set("X-Trace-Id", traceId)],
},
});Response validation
If the pact defines a response schema, the client validates the data before returning it. If validation fails, a ClientValidationError is thrown:
import { ClientValidationError } from "@routepact/client";
try {
const post = await request(PostPacts.getById, {
params: { id: "abc" },
});
} catch (err) {
if (err instanceof ClientValidationError) {
console.error(err.cause); // Standard Schema issues array
}
}If the pact has no response schema, the return type is never and no validation runs.
Multiple API instances
You can create multiple request functions pointing to different APIs:
export const internalRequest = createRequest(
internalKy,
"https://internal.example.com",
);
export const externalRequest = createRequest(
externalKy,
"https://api.partner.com",
);API reference
createRequest(kyInstance, baseUrl)
Returns an async function with the signature:
<TPact extends AnyRoutePact>(
pact: TPact,
options?: ClientRequestOptions<TPact>,
) => Promise<PactResponse<TPact>>;pact- a pact created withdefinePactfrom@routepact/coreoptions.params- required when the path contains:paramsegmentsoptions.payload- required forpost,patch,putwhen the pact has arequestschemaoptions.query- typed from the pact'squeryschema; required if the schema has required fieldsoptions.hooks- optional ky hooks for this specific request- Returns
PactResponse<TPact>- the schema's output type when a response schema is defined,undefinedotherwise
Type reference
| Export | Description |
| ----------------------------- | -------------------------------------------------------------------------------- |
| createRequest(ky, baseUrl) | Creates a typed request function bound to a ky instance and base URL |
| ClientRequestOptions<TPact> | Options accepted by the request function - params, payload, query, hooks |
| ClientValidationError | Thrown when response or meta validation fails - has field and cause |
