Skip to content
TypeWire

@tahanabavi/typewire-nestjs

v4.0.0

NestJS integration for the TypeWire ecosystem — serve one set of Zod contracts over every wire your client speaks: REST/HTTP, gRPC (Connect JSON), GraphQL and typesocket WebSocket gateways, with request/response validation on all of them.

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

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

DecoratorReturns
@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 declaresWire valueHandler 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:

json
{
"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():

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

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

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