A powerful TypeScript code generator that creates Zod schemas and type-safe clients from OpenAPI specifications.
- π₯ Zod Schema Generation: Automatically generate Zod validation schemas from OpenAPI component schemas
- π― Type-Safe Client: Generate a fully type-safe API client class with methods for each endpoint
- π‘ Multiple Formats: Support for OpenAPI 3.x specifications in JSON and YAML formats
- π Remote Files: Fetch OpenAPI specs from URLs using native fetch API
- β‘ Fast: Optimized for performance with minimal dependencies
- π§ Advanced Schema Support: Handles logical operators (anyOf, oneOf, allOf, not), enums, discriminators, and complex nested schemas
- π¦ Single File Output: Generates all schemas and client in one convenient TypeScript file
- π‘οΈ Runtime Validation: Built-in Zod validation for request/response data
- π Form Support: Supports both JSON and form-urlencoded request bodies
- π Extensible: Override
getBaseRequestOptions()to add authentication, custom headers, CORS, and other fetch options - π Response Handling: Override
handleResponse()to implement custom retry logic, logging, error handling, and catch 4xx/5xx with custom errors - π Server Configuration: Full support for OpenAPI server variables and templating (e.g.,
{environment}.example.com) - βοΈ Flexible Client Options: Options-based constructor supporting server selection, variable overrides, and custom base URLs
npm install -g zod-codegennpm install --save-dev zod-codegen# Generate from local file
zod-codegen --input ./openapi.json --output ./generated
# Generate from URL
zod-codegen --input https://api.example.com/openapi.json --output ./api
# Generate with custom output directory
zod-codegen -i ./swagger.yaml -o ./src/generated| Option | Alias | Description | Default |
|---|---|---|---|
--input |
-i |
Path or URL to OpenAPI file | Required |
--output |
-o |
Output directory (writes api.ts) or path to the generated file | generated |
--naming-convention |
-n |
Naming convention for operation IDs | (none) |
--help |
-h |
Show help | |
--version |
-v |
Show version |
The --naming-convention option allows you to transform operation IDs according to common naming conventions. Supported conventions:
camelCase- e.g.,getUserByIdPascalCase- e.g.,GetUserByIdsnake_case- e.g.,get_user_by_idkebab-case- e.g.,get-user-by-idSCREAMING_SNAKE_CASE- e.g.,GET_USER_BY_IDSCREAMING-KEBAB-CASE- e.g.,GET-USER-BY-ID
Example:
# Transform operation IDs to camelCase
zod-codegen --input ./openapi.json --output ./generated --naming-convention camelCase
# Transform operation IDs to snake_case
zod-codegen -i ./openapi.json -o ./generated -n snake_caseThis is particularly useful when OpenAPI specs have inconsistent or poorly named operation IDs.
import { Generator } from 'zod-codegen';
import type { GeneratorOptions } from 'zod-codegen';
// Create a simple reporter object
const reporter = {
log: (...args: unknown[]) => console.log(...args),
error: (...args: unknown[]) => console.error(...args)
};
// Create generator instance with naming convention
const generator = new Generator(
'my-app',
'1.0.0',
reporter,
'./openapi.json', // Input path or URL
'./generated', // Output directory
{
namingConvention: 'camelCase' // Transform operation IDs to camelCase
}
);
// Run the generator
const exitCode = await generator.run();For more advanced use cases, you can provide a custom transformer function that receives full operation details:
import { Generator } from 'zod-codegen';
import type { GeneratorOptions, OperationDetails } from 'zod-codegen';
const customTransformer: GeneratorOptions['operationNameTransformer'] = (details: OperationDetails) => {
// details includes: operationId, method, path, tags, summary, description
const { operationId, method, tags } = details;
// Example: Prefix with HTTP method and tag
const tag = tags?.[0] || 'default';
return `${method.toUpperCase()}_${tag}_${operationId}`;
};
const generator = new Generator('my-app', '1.0.0', reporter, './openapi.json', './generated', {
operationNameTransformer: customTransformer
});Note: Custom transformers take precedence over naming conventions if both are provided.
The generator creates a single TypeScript file (api.ts) containing:
- Zod Schemas: Exported Zod validation schemas for all component schemas defined in your OpenAPI spec
- API Client Class: A type-safe client class with methods for each endpoint operation
- ResponseValidationError: A generic error class thrown when response data fails Zod schema validation, carrying the original response and error details
- Server Configuration:
serverConfigurationsarray anddefaultBaseUrlconstant extracted from OpenAPI servers - Client Options Type:
ClientOptionstype for flexible server selection and variable overrides - Protected Extension Points:
getBaseRequestOptions()method for customizing request optionshandleResponse()method for response handling (retries, circuit breakers, etc.)
The generated client class includes:
export class YourAPI {
readonly #baseUrl: string;
// Options-based constructor (if servers are defined in OpenAPI spec)
constructor(options: ClientOptions);
// Or simple baseUrl constructor (if no servers defined)
constructor(baseUrl: string = '/', _?: unknown);
// Protected method - override to customize request options
protected getBaseRequestOptions(): Partial<Omit<RequestInit, 'method' | 'body'>>;
// Protected method - override to handle responses (retries, circuit breakers, etc.)
protected async handleResponse<T>(response: Response, method: string, path: string, options: {...}): Promise<Response>;
// Private method - handles all HTTP requests
async #makeRequest<T>(method: string, path: string, options: {...}): Promise<T>;
// Generated endpoint methods (one per operationId)
async yourEndpointMethod(...): Promise<ResponseType>;
}
// ClientOptions type (when servers are defined)
export type ClientOptions = {
baseUrl?: string; // Override base URL directly
serverIndex?: number; // Select server by index (0-based)
serverVariables?: Record<string, string>; // Override server template variables
};
// Server configuration (when servers are defined)
export const serverConfigurations: Array<{
url: string;
description?: string;
variables?: Record<string, {
default: string;
enum?: string[];
description?: string;
}>;
}>;
export const defaultBaseUrl: string; // First server with default variablesgenerated/
βββ api.ts # All schemas and client in one file
Given this OpenAPI specification:
openapi: 3.0.0
info:
title: User API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/users/{id}:
get:
operationId: getUserById
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
required: [id, name, email]Generated Output (generated/api.ts):
import { z } from 'zod';
// Components schemas
export const User = z.object({
id: z.number().int(),
name: z.string(),
email: z.string().email()
});
// Server configuration (when servers are defined in OpenAPI spec)
export const serverConfigurations = [
{
url: 'https://api.example.com'
}
];
export const defaultBaseUrl = 'https://api.example.com';
export type ClientOptions = {
baseUrl?: string;
serverIndex?: number;
serverVariables?: Record<string, string>;
};
// ResponseValidationError class
export class ResponseValidationError<T> extends Error {
readonly response: Response;
readonly error: z.ZodError<T>;
constructor(message: string, response: Response, error: z.ZodError<T>) {
super(message);
this.name = 'ResponseValidationError' as const;
this.response = response;
this.error = error;
}
get data(): T {
return this.response.json() as T;
}
}
// Client class
export class UserAPI {
readonly #baseUrl: string;
constructor(options: ClientOptions = {}) {
this.#baseUrl = options.baseUrl || defaultBaseUrl;
}
protected getBaseRequestOptions(): Partial<Omit<RequestInit, 'method' | 'body'>> {
return {};
}
async getUserById(id: number): Promise<User> {
const response = await this.makeRequest('GET', `/users/${id}`, {});
const parsedUser = User.safeParse(response);
if (!parsedUser.success) {
throw new ResponseValidationError<User>(`Invalid user: ...`, response, parsedUser.error);
}
return parsedUser.data;
}
// ... protected makeRequest method
}Usage:
import UserAPI from './generated/api';
// Use default server from OpenAPI spec
const client = new UserAPI({});
const user = await client.getUserById(123);
// user is fully typed and validated!The generated client includes a protected getBaseRequestOptions() method that you can override to customize request options. This method returns Partial<Omit<RequestInit, 'method' | 'body'>>, allowing you to configure:
- Headers: Authentication tokens, User-Agent, custom headers
- CORS:
mode,credentialsfor cross-origin requests - Request Options:
signal(AbortController),cache,redirect,referrer, etc.
Important: Options from getBaseRequestOptions() are merged with (not replaced by) request-specific options. Base options like mode, credentials, and signal are preserved, while headers are merged (base headers + Content-Type + request headers). See EXAMPLES.md for detailed merging behavior.
import UserAPI from './generated/api';
import type { ClientOptions } from './generated/api';
class AuthenticatedUserAPI extends UserAPI {
private accessToken: string | null = null;
constructor(options: ClientOptions = {}) {
super(options);
}
protected getBaseRequestOptions(): Partial<Omit<RequestInit, 'method' | 'body'>> {
const options = super.getBaseRequestOptions();
return {
...options,
headers: {
...((options.headers as Record<string, string>) || {}),
...(this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {})
}
};
}
setAccessToken(token: string): void {
this.accessToken = token;
}
}
// Usage
const client = new AuthenticatedUserAPI({});
client.setAccessToken('your-token-here');
const user = await client.getUserById(123); // Includes Authorization headerimport UserAPI from './generated/api';
import type { ClientOptions } from './generated/api';
class FullyConfiguredAPI extends UserAPI {
private accessToken: string | null = null;
constructor(options: ClientOptions = {}) {
super(options);
}
protected getBaseRequestOptions(): Partial<Omit<RequestInit, 'method' | 'body'>> {
const options = super.getBaseRequestOptions();
return {
...options,
headers: {
...((options.headers as Record<string, string>) || {}),
'User-Agent': 'MyApp/1.0.0',
...(this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {})
},
mode: 'cors',
credentials: 'include',
cache: 'no-cache'
};
}
setAccessToken(token: string): void {
this.accessToken = token;
}
}
// Usage
const client = new FullyConfiguredAPI({});
client.setAccessToken('your-token');You can set any RequestInit option except method and body (which are controlled by the generated code):
| Option | Type | Description |
|---|---|---|
headers |
HeadersInit |
Request headers (authentication, User-Agent, etc.) |
signal |
AbortSignal |
AbortController signal for request cancellation |
credentials |
RequestCredentials |
CORS credentials mode ('include', 'same-origin', 'omit') |
mode |
RequestMode |
Request mode ('cors', 'no-cors', 'same-origin', 'navigate') |
cache |
RequestCache |
Cache mode ('default', 'no-cache', 'reload', etc.) |
redirect |
RequestRedirect |
Redirect handling ('follow', 'error', 'manual') |
referrer |
string |
Referrer URL |
referrerPolicy |
ReferrerPolicy |
Referrer policy |
integrity |
string |
Subresource integrity hash |
keepalive |
boolean |
Keep connection alive |
See EXAMPLES.md for comprehensive examples including:
- Bearer token authentication
- Session management with token refresh
- Custom User-Agent headers
- CORS configuration
- Request cancellation with AbortController
- Environment-specific configurations
- Response handling with custom retry logic, 4xx/5xx error handling, and error transformation
Check out the examples directory for complete, runnable examples:
- Petstore API - Complete example with the Swagger Petstore API
- PokΓ©API - Example with a public REST API
Each example includes:
- Generated client code
- Basic usage examples
- Authentication examples
- README with instructions
- README.md - Project overview and quick start guide
- EXAMPLES.md - Comprehensive examples and patterns for extending the client
- CONTRIBUTING.md - Guidelines for contributing to the project
- CHANGELOG.md - Version history and changes
- Node.js β₯ 18.0.0 (see .nvmrc for recommended version)
- npm
# Clone the repository
git clone https://github.com/julienandreu/zod-codegen.git
cd zod-codegen
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm test
# Run linting
npm run lint
# Format code
npm run format# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Run tests with UI
npm run test:ui| Script | Description |
|---|---|
npm run build |
Build the project |
npm run build:watch |
Build in watch mode |
npm run dev |
Development mode with example |
npm test |
Run tests |
npm run test:watch |
Run tests in watch mode |
npm run test:coverage |
Run tests with coverage |
npm run lint |
Lint and fix code |
npm run format |
Format code with Prettier |
npm run type-check |
Type check without emitting |
npm run validate |
Run all checks (lint, format, type-check, test) |
npm run clean |
Clean build artifacts |
- Node.js: β₯ 18.0.0
- TypeScript: β₯ 5.9.3
- Zod: β₯ 4.1.12
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create your feature branch:
git checkout -b feature/amazing-feature - Make your changes
- Run tests:
npm test - Run validation:
npm run validate - Commit your changes:
git commit -m 'feat: add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- Zod - TypeScript-first schema validation
- OpenAPI Specification - API specification standard
- TypeScript - Typed JavaScript
- π Bug reports: GitHub Issues
- π¬ Questions: GitHub Discussions
- π§ Email: julienandreu@me.com
Made with β€οΈ by Julien Andreu