Retrofitting existing routes: @UseContract
Keep your own route decorators and add only validation:
@Controller("users")
export class UserController {
@Get(":id")
@UseContract(contracts.user.getUser)
getUser(@ContractPath() path: InferRequest<GetUser>["path"]) { ... }
}Param decorators
| Decorator | Returns |
|---|---|
@ContractInput() | The whole validated input, shaped like the contract's request schema — exactly what the frontend passed to the client method. |
@ContractPath() | Validated (and coerced) path params. |
@ContractQuery() | Validated (and coerced) query params. |
@ContractBody() | Validated body (the whole input for flat contracts). |
@ContractHeaders() | Validated headers part. |
Native @Param(), @Query(), and @Body() also see the validated, coerced values — the interceptor mirrors them back onto the platform request.
Wire-type coercion
HTTP turns everything in a URL into strings. The typefetch client serializes query/path values with URLSearchParams (true → "true", arrays → repeated keys, Date → ISO string, nested objects → JSON). typewire-nestjs reverses that against the contract schema before validating, so contracts written for the client work unchanged on the server:
| Contract declares | Wire value | Handler receives |
|---|---|---|
z.number() | "25" | 25 |
z.boolean() | "true" | true |
z.date() (query or body) | "2026-01-01T00:00:00.000Z" | Date |
z.array(z.string()) | ?tags=a&tags=b / ?tags=a | ["a","b"] / ["a"] |
z.object({...}) in query | '{"a":1}' (JSON string) | { a: 1 } |
z.bigint() in query | "9007199254740993" | 9007199254740993n |
Coercion never invents data — if a value can't be coerced it is passed through untouched and Zod reports the real error. Disable with coerce: false (per endpoint or globally).
Flat (non-structured) contracts
A request schema that isn't shaped as { path, query, body, headers } is "flat" — the client sends the whole input as the JSON body, and the server validates req.body against the whole schema. Both styles work with both decorators.