Skip to main content
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:
generate-client.ts
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

typescript
typeof ts
required
The TypeScript compiler API. Import from the typescript package.
routing
Routing
required
Your API routing configuration.
config
CommonConfig
required
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
clientClassName
string
default:"Client"
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).
hasHeadMethod
boolean
default:true
Generate HEAD method for each GET endpoint (Express feature).
brandHandling
object
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:
frontend-app.ts
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();

Pagination Support

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;
}

Formatting Options

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,
  },
});

Without Formatting

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:
generate-client.ts
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);
}
frontend-usage.ts
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