One of Express Zod API’s most powerful features is the ability to generate TypeScript clients that provide complete end-to-end type safety between your backend API and frontend applications.
Overview
The Integration class generates TypeScript code containing:
Input/output types for all your endpoints
A fully-typed client for making API requests
Runtime validation of request and response data
Support for Server-Sent Events (SSE) subscriptions
Quick Start
Create a script to generate your client:
import { writeFile } from "node:fs/promises" ;
import { Integration } from "express-zod-api" ;
import { routing } from "./routing" ;
import { config } from "./config" ;
import typescript from "typescript" ;
await writeFile (
"client.ts" ,
await new Integration ({
typescript ,
routing ,
config ,
serverUrl: "https://api.example.com" ,
}). printFormatted (),
"utf-8"
);
Run this script during your build process to keep your client in sync with your API.
Configuration Options
Basic Options
The TypeScript compiler API. Import from the typescript package.
Your API routing configuration.
Your API server configuration.
serverUrl
string
default: "https://example.com"
The base URL where your API is hosted.
Advanced Options
variant
'types' | 'client'
default: "client"
What to generate:
"types" - Only TypeScript types (for DIY solutions)
"client" - Full client with types and implementation
Name for the generated client class.
subscriptionClassName
string
default: "Subscription"
Name for the generated subscription class (for SSE).
noContent
z.ZodType
default: "z.undefined()"
Schema for responses without body (like 204 No Content).
Generate HEAD method for each GET endpoint (Express feature).
Custom handling rules for branded schemas. See the Integration class documentation for details.
Using the Generated Client
Basic Usage
The generated client provides type-safe methods for all your endpoints:
import { Client } from "./client" ;
const client = new Client ();
// TypeScript knows the exact shape of inputs and outputs
const response = await client . provide ( "get /v1/user/retrieve" , {
id: "10"
});
// response is fully typed based on your endpoint definition
console . log ( response . userName );
Path Parameters
The client automatically substitutes path parameters:
// If your route is /v1/user/:id
await client . provide ( "post /v1/user/:id" , {
id: "10" , // substituted into the path
name: "John" // sent as body
});
Custom Implementation
You can provide a custom implementation function to use your preferred HTTP library:
import { Client , Implementation } from "./client" ;
import axios from "axios" ;
const customImplementation : Implementation = async ({
method ,
url ,
body ,
headers ,
}) => {
const response = await axios ({
method ,
url ,
data: body ,
headers ,
});
return response . data ;
};
const client = new Client ( customImplementation );
Server-Sent Events (SSE)
For endpoints that use EventStreamFactory, use the generated Subscription class:
import { Subscription } from "./client" ;
const subscription = new Subscription ( "get /v1/events/stream" , {});
subscription . on ( "time" , ( timestamp ) => {
console . log ( "Server time:" , timestamp );
// TypeScript knows timestamp is a number based on your endpoint
});
subscription . on ( "error" , ( error ) => {
console . error ( "Stream error:" , error );
});
// Clean up when done
subscription . close ();
The client includes a hasMore() method for paginated endpoints:
import { Client } from "./client" ;
const client = new Client ();
let offset = 0 ;
const limit = 20 ;
while ( true ) {
const response = await client . provide ( "get /v1/users/list" , {
offset ,
limit ,
});
// Process users
response . users . forEach ( user => console . log ( user . name ));
// Check if more pages available
if ( ! client . hasMore ( response )) break ;
offset += limit ;
}
Using Prettier
The printFormatted() method automatically uses Prettier if installed:
const formatted = await integration . printFormatted ();
You can also provide custom formatting:
const formatted = await integration . printFormatted ({
format : async ( code ) => {
// Your custom formatter
return prettify ( code );
},
printerOptions: {
// TypeScript printer options
newLine: ts . NewLineKind . LineFeed ,
},
});
For unformatted output:
const code = integration . print ({
// Optional TypeScript printer options
newLine: ts . NewLineKind . LineFeed ,
});
Async Creation
If you want to avoid importing TypeScript yourself, use the async create() method:
import { Integration } from "express-zod-api" ;
const client = await Integration . create ({
routing ,
config ,
variant: "client" ,
// TypeScript is imported automatically
});
Types-Only Generation
For DIY solutions where you want to implement your own client:
const integration = new Integration ({
typescript ,
routing ,
config ,
variant: "types" , // Only generate types
});
This generates:
Input types for all endpoints
Response types for all endpoints
Path and method type unions
Request/response interfaces
Complete Example
Here’s a full example with multiple features:
import { writeFile } from "node:fs/promises" ;
import { Integration } from "express-zod-api" ;
import { routing } from "./routing" ;
import { config } from "./config" ;
import typescript from "typescript" ;
import { z } from "zod" ;
const integration = new Integration ({
typescript ,
routing ,
config ,
variant: "client" ,
clientClassName: "ApiClient" ,
subscriptionClassName: "ApiSubscription" ,
serverUrl: process . env . API_URL || "http://localhost:8080" ,
hasHeadMethod: true ,
noContent: z . undefined (),
});
try {
const output = await integration . printFormatted ();
await writeFile ( "src/api/client.ts" , output , "utf-8" );
console . log ( "✓ Client generated successfully" );
} catch ( error ) {
console . error ( "Failed to generate client:" , error );
process . exit ( 1 );
}
import { ApiClient , ApiSubscription } from "./api/client" ;
const api = new ApiClient ();
// Type-safe API calls
const user = await api . provide ( "get /v1/user/:id" , { id: "123" });
console . log ( user . name , user . email );
// Type-safe subscriptions
const events = new ApiSubscription ( "get /v1/events" , {});
events . on ( "update" , ( data ) => {
// data is fully typed
console . log ( "Update:" , data );
});
The generated client requires TypeScript 4.1 or higher to consume.
Benefits
Type Safety Compile-time verification of request parameters and response handling
Auto-Complete Full IDE support with autocomplete for all endpoints and their types
Refactoring Changes to your API automatically surface as TypeScript errors in frontend
Documentation Types serve as inline documentation for API consumers
Next Steps