npm package: @routepact/express
Description: Express adapter for type-safe route pacts - endpoint builders, request/response validation middleware, and router setup
Latest version: 0.1.13

# @routepact/express

Express adapter for `@routepact/core` pacts. Provides type-safe endpoint builders, request/response validation middleware, and a versioned router factory.

## Installation

```bash
npm install @routepact/express @routepact/core express
npm install -D @types/express
```

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

## Core concepts

### `defineEndpoint`

Wraps a handler with a pact. Instead of receiving the raw Express `req`/`res`, the handler receives a typed **context object** whose types are inferred directly from the pact's schemas.

```ts
import { defineEndpoint } from "@routepact/express";
import { PostPacts } from "../shared/pacts/post.pact";

const createPost = defineEndpoint({
  pact: PostPacts.create,
  handler: async ({ body, res }) => {
    // body is typed as { title: string; body: string } (from pact.request)
    const post = await db.posts.insert(body);

    // res.json only accepts the shape defined by PostPacts.create.response
    res.status(201).json(post);
  },
});
```

**Returning a value** is a shorthand for `res.json(value)` - the framework calls it for you:

```ts
const createPost = defineEndpoint({
  pact: PostPacts.create,
  handler: async ({ body }) => {
    return await db.posts.insert(body); // sent as res.json(post) automatically
  },
});
```

#### Context properties

| Property      | Type                      | Description                                                                     |
| ------------- | ------------------------- | ------------------------------------------------------------------------------- |
| `body`        | inferred from `request`   | Parsed and validated request body. `never` if the pact has no request schema.   |
| `params`      | inferred from path string | Path parameters (e.g. `{ id: string }` for `/posts/:id`). `never` if no params. |
| `query`       | inferred from `query`     | Validated query parameters. `never` if the pact has no query schema.            |
| `extensions`  | merged middleware returns | Typed object containing all additions returned by endpoint middlewares. `{}` if none. |
| `req`         | `Request`                 | Raw Express request - use for headers, cookies, IP, etc.                        |
| `res`         | `TypedServerResponse<TBody>` | Express response with `json()` typed to the pact's response schema.          |
| `next`        | `NextFunction`            | Express next function - use to forward errors to your error handler.            |

```ts
const getPostById = defineEndpoint({
  pact: PostPacts.getById, // path: "/posts/:id"
  handler: ({ params }) => {
    // params is typed as { id: string }
    return db.posts.findById(params.id);
  },
});

const listPosts = defineEndpoint({
  pact: PostPacts.list, // has query schema with optional page field
  handler: ({ query }) => {
    // query is typed as { page?: string }
    return db.posts.list({ page: query.page });
  },
});
```

For anything beyond body/params/query - headers, cookies, IP address, etc. - use `ctx.req`:

```ts
const createPost = defineEndpoint({
  pact: PostPacts.create,
  handler: ({ body, req }) => {
    const ip = req.ip;
    // ...
  },
});
```

### `createRouter`

Takes a config with a `basePath` and route groups and returns an Express router mounted at that path.

```ts
import { createRouter } from "@routepact/express";
import {
  createPost,
  getPostById,
  listPosts,
  updatePost,
  deletePost,
} from "./post.endpoints";

export const postRouter = createRouter({
  basePath: "/api/v1",
  routeGroups: [
    {
      description: "Posts",
      endpoints: [listPosts, getPostById, createPost, updatePost, deletePost],
    },
  ],
});

// Mount in your Express app
app.use(postRouter);
// Routes are now available at /api/v1/posts, /api/v1/posts/:id, etc.
```

**Options:**

```ts
createRouter(config, {
  logger: myLogger, // default: console - must have an `error` method
});
```

The router detects and throws on duplicate route registrations (`[method] path` pairs) at startup rather than silently shadowing them.

### `defineRouteGroup`

Helper to define a route group with full type checking before passing it to `createRouter`:

```ts
import { defineRouteGroup } from "@routepact/express";

const publicRoutes = defineRouteGroup({
  description: "Public routes",
  endpoints: [listPosts, getPostById],
});
```

## Middleware

### Group middleware

Apply Express middleware to every endpoint in a route group. Common use case: protecting a group of routes with an auth check.

```ts
import { createRouter } from "@routepact/express";
import { requireAuth } from "./middlewares/auth";
import { listPosts, createPost } from "./post.endpoints";

const router = createRouter({
  basePath: "/api/v1",
  routeGroups: [
    {
      description: "Public routes",
      endpoints: [listPosts],
    },
    {
      description: "Protected routes",
      middlewares: [requireAuth],
      endpoints: [createPost],
    },
  ],
});
```

### Endpoint middleware (`createEndpointMiddleware`)

Type-safe middleware that adds properties directly to the handler context. The properties returned by the middleware are merged into the context type seen by the handler.

```ts
import { createEndpointMiddleware, defineEndpoint } from "@routepact/express";
import { PostPacts } from "../shared/pacts/post.pact";
import { getSessionFromRequest } from "./auth";

// Declare what this middleware adds to the context
const withUser = createEndpointMiddleware(async (req) => {
  const session = await getSessionFromRequest(req);
  if (!session) throw new Error("Unauthorized");
  return { user: session.user }; // TypeScript infers { user: User }
});

const createPost = defineEndpoint({
  pact: PostPacts.create,
  middlewares: [withUser],
  handler: async ({ body, extensions }) => {
    // extensions.user is typed as User - no casting needed
    return db.posts.insert({ ...body, authorId: extensions.user.id });
  },
});
```

Multiple middlewares are supported and their added types are merged:

```ts
const getPost = defineEndpoint({
  pact: PostPacts.getById,
  middlewares: [withUser, withRateLimit],
  handler: async ({ params, extensions }) => {
    // extensions has both .user (from withUser) and .rateLimit (from withRateLimit)
  },
});
```

## Validation middleware

Request and response validation is applied automatically when a pact has the corresponding schemas. You don't need to call these manually unless you want to use them outside of `createRouter`.

```ts
import {
  createRequestValidationMiddleware,
  createResponseValidationMiddleware,
} from "@routepact/express";
import { z } from "zod";

// Validates req.body against the schema, result available as ctx.body
app.use(createRequestValidationMiddleware(z.object({ name: z.string() })));

// Validates res.json(...) before sending
app.use(
  createResponseValidationMiddleware(
    z.object({ id: z.string(), name: z.string() }),
  ),
);
```

### Validation errors

When validation fails, the middleware calls `next(error)` with one of these:

| Error class               | Status | When                                       |
| ------------------------- | ------ | ------------------------------------------ |
| `RequestValidationError`  | 400    | `req.body` or `req.query` fails validation |
| `ResponseValidationError` | 500    | `res.json(...)` fails the response schema  |

Both expose a `cause` property with the Standard Schema issues array. Register an error handler in your Express app to format them:

```ts
import {
  ValidationError,
  RequestValidationError,
  ResponseValidationError,
} from "@routepact/express";

app.use((err, req, res, next) => {
  if (err instanceof RequestValidationError) {
    return res
      .status(400)
      .json({ message: "Invalid request", errors: err.cause });
  }
  if (err instanceof ResponseValidationError) {
    console.error("Response validation failed:", err.cause);
    return res.status(500).json({ message: "Internal server error" });
  }
  // Or catch any validation error regardless of type:
  if (err instanceof ValidationError) {
    return res.status(err.status).json({ message: err.message });
  }
  next(err);
});
```

## Type reference

| Export                                        | Description                                                                |
| --------------------------------------------- | -------------------------------------------------------------------------- |
| `defineEndpoint(config)`                      | Creates a typed endpoint from a pact + handler                             |
| `defineRouteGroup(group)`                     | Creates a typed route group                                                |
| `createEndpointMiddleware(handler)`           | Creates a type-augmenting middleware                                       |
| `createRouter(config, options?)`              | Builds a versioned Express router                                          |
| `createRequestValidationMiddleware(schema)`   | Validates `req.body` and stores result in handler context as `body`        |
| `createResponseValidationMiddleware(schema?)` | Validates `res.json(...)` output before sending                            |
| `ValidationError`                             | Base class for all validation errors - has `status` and `cause: StandardSchemaV1.Issue[]` |
| `RequestValidationError`                      | Extends `ValidationError` - thrown on bad request body or query (400)      |
| `ResponseValidationError`                     | Extends `ValidationError` - thrown on bad response body (500)              |
| `PactHandlerContext<TPact, TMiddlewares>`     | The typed context object passed to every handler - middleware additions are under `.extensions` |
| `TypedServerResponse<TBody>`                  | Express `Response` with `json()` narrowed to `TBody`                       |
| `PactServerRequest`                           | Express `Request` extended with the `_routepact` namespace                 |
| `PactServerRequestHandler`                    | Request handler using `PactServerRequest` and `PactServerResponse`         |
| `PactServerResponse<TPact>`                   | `TypedResponse` specialised for a pact's response schema                   |
| `EndpointMiddleware<TAdds>`                   | A middleware that augments the handler context with `TAdds`                |
| `RouteEndpoint<TPact, TMiddlewares>`          | Full endpoint type including handler and middlewares                       |
| `RouteGroup`                                  | A group of endpoints with optional shared middleware                       |
| `RouterConfig`                                | Config passed to `createRouter`                                            |
| `RouterOptions`                               | Options passed to `createRouter` - `logger`                                |
| `Logger`                                      | `Pick<Console, "error">` - the logger interface expected by `createRouter` |