Global configuration
The decorators work with zero setup. Import the module to change defaults app-wide:
@Module({
imports: [
TypeFetchModule.forRoot({
validateRequest: true, // default
validateResponse: true, // default
coerce: true, // default
exposeResponseErrors: process.env.NODE_ENV !== "production",
}),
],
})
export class AppModule {}Every option can also be overridden per endpoint: @TypeFetchEndpoint(endpoint, { validateResponse: false }).
Field-level encryption (mirror of encryptionMiddleware)
When a contract endpoint sets an encryption config, the client's encryptionMiddleware encrypts the marked request fields before sending and decrypts the marked response fields on arrival. The backend mirrors it — decrypting request fields before validation and encrypting response fields after validation — using the same key material and the same algorithm (byte-compatible, via crypto-js / node-forge).
// shared contract
const contracts = {
auth: {
login: {
method: "POST",
path: "/login",
encryption: {
method: "AES", // AES | DES | RSA | Base64 | Custom
request: { password: true }, // decrypt before validation
response: { token: true }, // encrypt after validation
},
request: z.object({ body: z.object({ username: z.string(), password: z.string().min(6) }) }),
response: z.object({ token: z.string(), user: z.string() }),
},
},
} as const;Provide the same keyProvider the client uses:
TypeFetchModule.forRoot({
encryption: {
keyProvider: async () => ({ type: "symmetric", key: process.env.ENC_KEY! }),
// RSA: () => ({ type: "rsa", publicKey, privateKey })
// customHandlers: { encrypt, decrypt } // for method: "Custom"
// failClosed: true (default) — never leak plaintext on crypto failure
},
})Then handlers just work in plaintext:
@TypeFetchEndpoint(contracts.auth.login)
login(@ContractInput() input: InferRequest<typeof contracts.auth.login>) {
// input.body.password is already decrypted AND validated (min(6) ran on plaintext)
return { token: signJwt(input.body.username), user: input.body.username };
// token is encrypted on the way out; the client decrypts it
}Details that keep it interoperable:
Direction is mirrored — the client encrypts requests / decrypts responses, so the server decrypts requests / encrypts responses.
Order matters — request fields are decrypted before validation (so
min(6)etc. run on plaintext), response fields are encrypted after validation.Value serialization matches — non-string values are
JSON.stringifyd before encryption andsafeJsonParsed after decryption, exactly like the client, so numbers/objects round-trip.Per-direction methods —
method: { request: "AES", response: "Base64" }is honored; a bare string applies to both. Deep maps ({ profile: { pin: true } }) and the{ body: { ... } }request style are supported.crypto-jsandnode-forgeare optional peers — required lazily, only when encryption is used.failClosed(defaulttrue) turns any crypto failure into a400 DECRYPTION_ERROR/500 ENCRYPTION_ERRORrather than leaking plaintext.
OpenAPI / Swagger from contracts
The same contracts generate a full OpenAPI 3.0 document — method, path, params, request body, and responses all derived from the Zod schemas, so your API docs can never drift from what the client calls.
import { NestFactory } from "@nestjs/core";
import { setupContractSwagger } from "@tahanabavi/typewire-nestjs";
import { contracts } from "./contracts";
const app = await NestFactory.create(AppModule);
setupContractSwagger(app, contracts, {
path: "docs",
info: { title: "My API", version: "1.0.0" },
});
await app.listen(3000);
// Swagger UI → http://localhost:3000/docs
// Raw JSON → http://localhost:3000/docs-json@nestjs/swagger is an optional peer dependency — install it only if you use setupContractSwagger(). Prefer to serve the document yourself? Build the plain object directly:
import { buildOpenApiDocument } from "@tahanabavi/typewire-nestjs";
const document = buildOpenApiDocument(contracts, { info: { title: "My API", version: "1.0.0" } });
// hand to SwaggerModule.setup(), write to disk, feed a client generator, ...What the generator maps from each contract endpoint:
| Contract | OpenAPI |
|---|---|
path: "/users/:id" | /users/{id} with a required id path parameter |
request.path / request.query / request.headers | typed parameters (array query params → explode: true, matching repeated-key serialization) |
request.body (or a flat request) | requestBody — application/json, or multipart/form-data when bodyType: "form-data" |
response | the success response (201 for POST, 200 otherwise) |
z.date() / z.bigint() / file fields | string+date-time / string+int64 / string+binary (never throws) |
auth: true | bearerAuth security requirement + documented 401 |
| — | a shared ContractValidationError schema on every 400 |
Options: bearerAuth (default on), includeValidationError (default on), servers, and successStatus(endpoint) to override the documented success code.