@SkipEnvelope()
The response envelope is app-wide, which is right for an API whose client reads one wrapper shape — and wrong for anything whose body shape is fixed by someone else: a health check a load balancer parses, a webhook receipt, an OAuth callback.
@Get("/healthz")
@SkipEnvelope()
health() {
return { status: "ok" };
}It exempts the success wrapper and the error branch, so a failing exempt route answers with its own shape too. gRPC routes and the GraphQL endpoint carry it automatically: each brings an envelope of its own.
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).
Error shapes (RichError-compatible)
Request validation failure → 400 with all part issues collected:
{
"statusCode": 400,
"message": "Request validation failed",
"code": "VALIDATION_ERROR",
"errors": {
"path.id": ["Too small: expected string to have >=1 characters"],
"body.name": ["Too small: expected string to have >=2 characters"]
}
}On the frontend, RichError picks up message, code, and errors automatically — field errors from the backend arrive typed and addressable.
Response contract violation → 500 with code: "RESPONSE_CONTRACT_VIOLATION". Issues are always logged server-side; include them in the body during development with exposeResponseErrors: true.
Response envelope (mirror of setResponseWrapper)
The typefetch client can wrap every response in an envelope via client.setResponseWrapper():
client.setResponseWrapper((successResponse) =>
z.union([
z.object({ success: z.literal(true), data: successResponse }),
z.object({ success: z.literal(false), message: z.string() }),
])
)Enable the matching server side so the client's wrapper parses both branches:
TypeFetchModule.forRoot({ envelope: true })Successful responses are wrapped in
{ success: true, data }— after contract-response validation (the envelope interceptor is global, so it runs outside the method-scoped validator).Every error — contract 400s, response-violation 500s, and any other
HttpException— is formatted by a catch-all filter into{ success: false, message, code?, errors? }, keeping the original HTTP status. This matters: with a client wrapper active, an unwrapped error body would fail the client's schema parse, so the envelope must cover failures too.It applies to all routes (contract-bound or not), so the whole API has one shape.
Customize the shape (must match your client wrapper) or return errors as 200:
TypeFetchModule.forRoot({
envelope: {
success: (data) => ({ ok: true, result: data }),
error: (e) => ({ ok: false, reason: e.message, code: e.code }),
errorStatus: 200, // default "preserve" keeps the real HTTP status
},
})e is { message, status, code?, errors? }. Disabled by default; ResponseEnvelopeInterceptor and ContractEnvelopeExceptionFilter are also exported for manual wiring.