npm package: @routepact/client
Description: Ky-based type-safe HTTP client for route pacts - validates responses against Zod schemas
Latest version: 0.1.13

# @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

```bash
npm install @routepact/client @routepact/core ky
```

You also need a schema library that implements the [Standard Schema](https://standardschema.dev/) interface (e.g. Zod, Valibot, ArkType) for defining your pacts. Examples below use Zod but any Standard Schema-compatible library works.

```bash
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.

```ts
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.

```ts
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:

```ts
// 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=2
```

If 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.

```ts
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 instance
```

You can also pass per-request ky hooks via the `hooks` option:

```ts
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:

```ts
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:

```ts
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:

```ts
<TPact extends AnyRoutePact>(
  pact: TPact,
  options?: ClientRequestOptions<TPact>,
) => Promise<PactResponse<TPact>>;
```

- `pact` - a pact created with `definePact` from `@routepact/core`
- `options.params` - required when the path contains `:param` segments
- `options.payload` - required for `post`, `patch`, `put` when the pact has a `request` schema
- `options.query` - typed from the pact's `query` schema; required if the schema has required fields
- `options.hooks` - optional ky hooks for this specific request
- Returns `PactResponse<TPact>` - the schema's output type when a response schema is defined, `undefined` otherwise

## 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`          |