npm package: @ivancerovina/contracts-nestjs
Description: NestJS integration for @ivancerovina/contracts — route binding, Zod validation, response envelope, error handling
Author: Ivan Cerovina
Latest version: 1.0.0

# @ivancerovina/contracts-nestjs

NestJS integration for `@ivancerovina/contracts`. Binds contract routes to controller methods — sets HTTP method, path, status code, Zod validation, response envelope wrapping, and error handling from a single decorator.

## Install

```bash
pnpm add @ivancerovina/contracts-nestjs @ivancerovina/contracts
```

Peer dependencies: `@nestjs/common >= 10`, `@nestjs/core >= 10`.

## Usage

Use `@Controller()` as normal. Decorate each method with `@BindContract(contract, "routeName")` — the route name autocompletes from the contract's routes.

```typescript
import { Controller, Param, Query, Body } from "@nestjs/common";
import { BindContract, ContractError } from "@ivancerovina/contracts-nestjs";
import { TaskContract } from "./task.contract";

@Controller(TaskContract.baseRoute)
export class TaskController {
  constructor(private readonly tasks: TaskService) {}

  @BindContract(TaskContract, "listTasks")
  async listTasks(@Query() query: { page: number; limit: number; status?: string }) {
    return this.tasks.findAll(query);
  }

  @BindContract(TaskContract, "getTask")
  async getTask(@Param("taskId") taskId: string) {
    const task = await this.tasks.findById(taskId);
    if (!task) throw new ContractError("TASK_NOT_FOUND", "Task not found", 404);
    return task;
  }

  @BindContract(TaskContract, "createTask")
  async createTask(@Body() body: { title: string; projectId: string }) {
    return this.tasks.create(body);
  }
}
```

Register the controller normally with `@Module({ controllers: [TaskController] })`.

## What `@BindContract` does

For each decorated method, the decorator:

| Concern | What it sets |
|---------|-------------|
| **HTTP method** | `GET`, `POST`, `PATCH`, `PUT`, `DELETE` from `routeDef.method` |
| **Path** | `routeDef.path` (relative to `@Controller` prefix) |
| **Status code** | `201` for POST, `200` for everything else |
| **Validation** | Runs `routeDef.params`, `routeDef.query`, `routeDef.body` through Zod `.safeParse()` before the handler executes. Mutates `req.params`/`req.query`/`req.body` with parsed values so `@Param()`, `@Query()`, `@Body()` receive validated data. |
| **Response envelope** | Attaches `ContractInterceptor` — wraps return values in `{ success: true, data, requestId }` |
| **Error handling** | Attaches `ContractErrorFilter` — catches `ContractError` and returns `{ success: false, error: { message, code, details? }, requestId }` |

You still use standard NestJS decorators (`@Param()`, `@Query()`, `@Body()`, `@UseGuards()`, etc.) — `@BindContract` composes with them.

## `ContractError`

Throw from handlers or services to produce typed error responses.

```typescript
throw new ContractError(code, message, status);
throw new ContractError(code, message, status, zodError);
```

| Param | Type | Description |
|-------|------|-------------|
| `code` | `string` | Error code from the contract's `errors` (e.g., `"TASK_NOT_FOUND"`) |
| `message` | `string` | Human-readable message |
| `status` | `number` | HTTP status code |
| `validationErrors` | `ZodError?` | Optional — attached automatically for validation failures |

### Error response

```json
{
  "success": false,
  "error": {
    "message": "Task not found",
    "code": "TASK_NOT_FOUND"
  },
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}
```

Validation errors include field-level details:

```json
{
  "success": false,
  "error": {
    "message": "Invalid request body",
    "code": "BAD_REQUEST",
    "details": [
      { "path": ["title"], "message": "String must contain at least 1 character(s)" }
    ]
  },
  "requestId": "..."
}
```

## Success response

The `ContractInterceptor` wraps handler return values automatically:

```json
{
  "success": true,
  "data": { "id": "...", "title": "...", "status": "todo" },
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}
```

`requestId` is resolved from `req.id` (set by logging middleware), `x-request-id` header, or a generated UUID.

## Type helpers

`InputFor` is a shorthand for typing handler parameters when you want the full parsed input object:

```typescript
import type { InputFor } from "@ivancerovina/contracts-nestjs";

type CreateTaskInput = InputFor<typeof TaskContract, "createTask">;
// { body: { title: string; projectId: string; ... } }

type GetTaskInput = InputFor<typeof TaskContract, "getTask">;
// { params: { taskId: string } }
```

`RouteInput<R>` does the same for a single route definition.

## Exports

| Export | Description |
|--------|-------------|
| `BindContract(contract, routeName)` | Method decorator — wires routing, validation, envelope, error filter |
| `ContractError` | Throwable error class with `code`, `message`, `status`, `validationErrors?` |
| `ContractInterceptor` | Response envelope interceptor (attached automatically by `@BindContract`) |
| `ContractErrorFilter` | Exception filter for `ContractError` (attached automatically by `@BindContract`) |
| `InputFor<C, K>` | Type helper — parsed input for a contract route |
| `RouteInput<R>` | Type helper — parsed input for a route definition |

## Scripts

```bash
pnpm build       # Build with tsdown
pnpm dev         # Watch mode
pnpm lint        # Biome check
```