NestJS integration for the TypeWire ↗ ecosystem — bind handlers and validate request input and response output on the backend using the exact same Zod contracts your frontend consumes, over every wire the client speaks: REST/HTTP, gRPC (Connect JSON), GraphQL, and typesocket WebSocket gateways.
One contract file. The client validates on the way out; typewire-nestjs validates on the way in — and guarantees your handlers return exactly what the contract promises, whichever protocol carried it.
frontend shared contract backend
┌──────────────────────┐ ┌─────────────────────────┐ ┌──────────────────────┐
│ ApiClient(contracts)│ ─────▶ │ { transport?, │ ◀───── │ @TypeFetchEndpoint( │
│ api.user.getUser() │ │ request: z.object, │ │ contracts.user. │
│ ✓ input validated │ │ response: z.object } │ │ getUser) │
│ ✓ output validated │ └─────────────────────────┘ │ ✓ route from path │
└──────────────────────┘ one file │ ✓ input validated │
│ │ ✓ output validated │
┌───────────────────────────────────┼───────────────────────────────────┐
▼ ▼ ▼ ▼ ▼
@TypeFetchEndpoint @GrpcEndpoint @GraphQLEndpoint @SocketEvent one pipeline
http · REST grpc · Connect graphql ws · typesocket guards, validation,
permissionEvery wire the client speaks
typefetch v2 made the transport pluggable: one client, one contract file, and
transport: "grpc" or transport: "graphql" on the endpoints that need it.
This package serves all of them, plus typesocket gateways.
Each wire is bound by its own decorator from its own entry point — the mirror of the client, where an adapter is registered at the setup site rather than discovered. If the server could silently serve a transport the client never registered, the two halves of one contract could disagree about which wire they are on.
| The contract declares | Bind it with | Import from | Also install |
|---|---|---|---|
nothing, or transport: "http" | @TypeFetchEndpoint() | @tahanabavi/typewire-nestjs | — |
transport: "grpc" | @GrpcEndpoint() | …/typewire-nestjs/grpc | @tahanabavi/typefetch-grpc |
transport: "graphql" | @GraphQLEndpoint() | …/typewire-nestjs/graphql | @tahanabavi/typefetch-graphql |
| a typesocket event | @SocketEvent() | …/typewire-nestjs/socket | @nestjs/websockets |
Every peer above is one you already have: @tahanabavi/typefetch-grpc is what
augments typefetch's TransportRegistry with transport: "grpc", so without it
the contract could not have been written.
Passing a contract to the wrong decorator throws while the module is loading, naming the one that serves it:
[typewire-nestjs] @TypeFetchEndpoint() serves "http" endpoints, but
user.v1.UserService/GetUser is declared for "grpc". Bind it with
`@GrpcEndpoint()` from "@tahanabavi/typewire-nestjs/grpc" instead.Everything below the transport is shared. Guards (including the permission
guard), interceptors, request validation against request and response
validation against response work identically on all four — only the wire
differs, which is the whole claim of the transport seam.