@cerios/openapi-to-zod-playwright
v1.1.2
Published
Generate type-safe Playwright API clients from OpenAPI specifications with Zod validation
Readme
@cerios/openapi-to-zod-playwright
Generate type-safe Playwright API clients from OpenAPI specifications with Zod validation.
Features
Playwright-Specific Features
- 🎭 Playwright Integration: Uses
ApiRequestContextfor API testing - 🎯 Two-Layer Architecture: Thin client layer + validated service layer
- 🧪 Testing Friendly: Separate error methods for testing failure scenarios
- 📝 Status Code Validation: Uses Playwright's
expect()for status checks - 🔄 Multiple Responses: Separate methods per status code when needed
- 📁 File Splitting: Separate files for schemas, client, and service layers
- 🎨 Method Naming: Automatic method names from paths or operationIds
- 🔍 Status Code Filtering: Include/exclude specific status codes (e.g.,
includeStatusCodes: ["2xx"]) - 🚫 Header Filtering: Ignore specific headers with glob patterns (e.g.,
ignoreHeaders: ["Authorization"]) - 🔗 Base Path Support: Prepend common base paths to all endpoints (e.g.,
basePath: "/api/v1") - ✅ Request Validation: Optional Zod validation for request bodies (
validateServiceRequest) - 🏷️ Operation Filtering: Filter by tags, paths, methods, deprecated status, and more
Core Features (from @cerios/openapi-to-zod)
For complete Zod schema generation features, see the @cerios/openapi-to-zod README:
- ✅ Zod v4 Compatible with latest features
- 📝 TypeScript Types from
z.infer - 🔧 Flexible Modes: Strict, normal, or loose validation
- 📐 Format Support: uuid, email, url, date, etc.
- 🔀 Discriminated Unions: Automatic
z.discriminatedUnion() - 🔐 readOnly/writeOnly: Separate request/response schemas
- 📋 Constraint Support: multipleOf, additionalProperties, etc.
- And many more schema generation features...
Installation
npm install @cerios/openapi-to-zod-playwright @cerios/openapi-to-zod @playwright/test zodNote:
@cerios/openapi-to-zodis required as a peer dependency for shared utilities and the core functionality.
Quick Start
1. Initialize Configuration
npx openapi-to-zod-playwright initThis will guide you through creating a configuration file:
? Input OpenAPI file path: openapi.yaml
? Output TypeScript file path: tests/api-client.ts
? Config file format: TypeScript (recommended)
? Include commonly-used defaults? YesCreates openapi-to-zod-playwright.config.ts:
import { defineConfig } from '@cerios/openapi-to-zod-playwright';
export default defineConfig({
defaults: {
mode: 'normal',
validateServiceRequest: false,
},
specs: [
{
input: 'openapi.yaml',
output: 'tests/api-client.ts',
},
],
});2. Generate Client
npx openapi-to-zod-playwright3. Use in Tests
import { test, expect } from '@playwright/test';
import { ApiClient, ApiService } from './api-client';
test('create and get user', async ({ request }) => {
const client = new ApiClient(request);
const service = new ApiService(client);
// Create user - validates request and response
const user = await service.postUsers201({
data: {
email: '[email protected]',
name: 'Test User',
age: 25
}
});
expect(user.email).toBe('[email protected]');
// Get user by ID
const fetchedUser = await service.getUsersByUserId(user.id);
expect(fetchedUser.id).toBe(user.id);
});
test('handle error response', async ({ request }) => {
const client = new ApiClient(request);
const service = new ApiService(client);
// Test error scenario
const response = await service.postUsersError({
data: { email: 'invalid' } // Missing required 'name'
});
expect(response.status()).toBe(400);
});Architecture
This package generates up to three separate files:
1. Schemas (Always Generated)
- Zod validation schemas for all request/response types
- TypeScript type definitions via
z.infer - Located in the main
outputfile
// Generated schemas
export const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string()
});
export type User = z.infer<typeof userSchema>;2. ApiClient (Optional - Thin Passthrough Layer)
Generated when outputClient is specified:
- Direct passthrough to Playwright's
ApiRequestContext - No validation - allows testing invalid requests
- All request properties are
Partial<>for flexibility - Returns raw
APIResponse
const client = new ApiClient(request);
// Can send invalid data for testing
const response = await client.postUsers({
data: { invalid: 'data' }
});3. ApiService (Optional - Validated Layer)
Generated when outputService is specified (requires outputClient):
- Validates requests with Zod schemas (when
validateServiceRequest: true) - Validates response status with Playwright
expect() - Validates response bodies with Zod schemas
- Separate methods per status code for multiple responses
- Generic error methods for testing failures
const service = new ApiService(client);
// Validates request and response - throws on invalid data
const user = await service.postUsers201({
data: { email: '[email protected]', name: 'Test' }
});Method Naming
Methods are named using the pattern: {httpMethod}{PascalCasePath}{StatusCode?}
Examples:
GET /users→getUsers()(single 200 response)POST /users→postUsers201()(201 response)GET /users/{userId}→getUsersByUserId(userId: string)(single 200 response)DELETE /users/{userId}→deleteUsersByUserId(userId: string)(204 response)POST /users→postUsersError()(error testing method)
Status Code Suffixes
- Single response: No suffix (e.g.,
getUsers()) - Multiple responses: Status code suffix per method (e.g.,
postUsers200(),postUsers201()) - Error methods:
Errorsuffix for testing failures (e.g.,getUsersByUserIdError())
Path Prefix Stripping
The stripPathPrefix option removes common prefixes from API paths before generating method names and documentation. This creates cleaner, more readable method names while keeping the actual HTTP requests intact when combined with basePath.
Basic Usage
export default defineConfig({
specs: [{
input: 'openapi.yaml',
output: 'client.ts',
stripPathPrefix: '/api/v1.0', // Strip this prefix from all paths
basePath: '/api/v1.0' // Add it back for HTTP requests
}]
});How It Works
OpenAPI Spec:
paths:
/api/v1.0/users:
get:
summary: Get all users
/api/v1.0/posts:
get:
summary: Get all postsWithout stripPathPrefix:
// Method names generated from full path
getApiV10Users() // GET /api/v1.0/users
getApiV10Posts() // GET /api/v1.0/postsWith stripPathPrefix: '/api/v1.0':
// Method names generated from stripped path (cleaner)
getUsers() // GET /users (shown in JSDoc)
getPosts() // GET /posts (shown in JSDoc)
// Actual HTTP requests use basePath
// GET {baseURL}/api/v1.0/users
// GET {baseURL}/api/v1.0/postsGlob Patterns
Use glob patterns to strip dynamic prefixes (e.g., version numbers):
export default defineConfig({
specs: [{
input: 'openapi.yaml',
output: 'client.ts',
// Strip any versioned API prefix using wildcards
stripPathPrefix: '/api/v*'
}]
});Matches:
/api/v1.0/users→/users/api/v2.5/posts→/posts/api/v10.3/products→/products
Glob Pattern Syntax:
Glob patterns support powerful matching using minimatch:
*matches any characters within a single path segment (stops at/)**matches any characters across multiple path segments (crosses/boundaries)?matches a single character[abc]matches any character in the set{a,b}matches any of the alternatives!(pattern)matches anything except the pattern
// Examples of glob patterns:
stripPathPrefix: '/api/v*' // Matches /api/v1, /api/v2, /api/v10
stripPathPrefix: '/api/**/v1' // Matches /api/v1, /api/internal/v1, /api/public/v1
stripPathPrefix: '/api/v*.*' // Matches /api/v1.0, /api/v2.5
stripPathPrefix: '/api/v[0-9]' // Matches /api/v1, /api/v2
stripPathPrefix: '/api/{v1,v2}' // Matches /api/v1 or /api/v2
stripPathPrefix: '/!(internal)/**' // Matches any path except those starting with /internal/Normalization
stripPathPrefix handles various input formats:
// All of these work the same:
stripPathPrefix: '/api/v1' // Preferred
stripPathPrefix: 'api/v1' // Normalized to /api/v1
stripPathPrefix: '/api/v1/' // Trailing slash removedCommon Patterns
Pattern 1: Clean Version Prefixes
{
stripPathPrefix: '/api/v1.0',
basePath: '/api/v1.0'
}
// Paths: /api/v1.0/users → getUsers() → GET /api/v1.0/usersPattern 2: Multiple API Versions with Wildcard
{
stripPathPrefix: '/api/v*',
basePath: '/api/v2' // Or determined at runtime
}
// All versions stripped, base path can varyPattern 3: Versioned Paths with Dots
{
stripPathPrefix: '/api/v*.*',
basePath: '/api/v2.0'
}
// Matches: /api/v1.0, /api/v2.5, etc.Pattern 4: Organization Prefix
{
stripPathPrefix: '/myorg/api',
basePath: '/myorg/api'
}
// Paths: /myorg/api/users → getUsers() → GET /myorg/api/usersBenefits
- Cleaner Method Names: Generates
getUsers()from path/usersinstead ofgetApiV10Users()from/api/v1.0/users - Better JSDoc: Shows
/usersinstead of/api/v1.0/usersin documentation - Flexible Routing: Strip prefix for naming, add back with
basePathfor requests - Version Independence: Use glob patterns to handle multiple API versions
CLI Usage
Initialize a New Config File
npx openapi-to-zod-playwright initInteractive prompts guide you through:
- Input OpenAPI file path
- Output file path
- Config format (TypeScript or JSON)
- Whether to include common defaults
Generate from Config
# Auto-discovers openapi-to-zod-playwright.config.{ts,json}
npx openapi-to-zod-playwright
# Or specify config file explicitly
npx openapi-to-zod-playwright --config path/to/config.tsOther Commands
# Display version
npx openapi-to-zod-playwright --version
# Display help
npx openapi-to-zod-playwright --help
# Display help for init command
npx openapi-to-zod-playwright init --helpConfiguration File
TypeScript Configuration (Recommended)
openapi-to-zod-playwright.config.ts:
import { defineConfig } from '@cerios/openapi-to-zod-playwright';
export default defineConfig({
defaults: {
mode: 'strict',
includeDescriptions: true,
showStats: false,
validateServiceRequest: false, // Optional request validation
},
specs: [
{
input: 'specs/api-v1.yaml',
output: 'src/generated/api-v1.ts'
},
{
input: 'specs/api-v2.yaml',
output: 'src/generated/api-v2.ts',
outputClient: 'src/generated/api-v2-client.ts',
outputService: 'src/generated/api-v2-service.ts',
stripPathPrefix: '/api/v2', // Strip prefix from paths for cleaner method names
basePath: '/api/v2', // Prepend base path to all endpoints
mode: 'normal',
prefix: 'v2',
ignoreHeaders: ['Authorization', 'X-*'], // Ignore specific headers
operationFilters: {
includeTags: ['public'], // Only include operations with 'public' tag
includeStatusCodes: ['2xx', '4xx'], // Only generate for success and client errors
},
useOperationId: true, // Use operationId from spec for method names
}
],
executionMode: 'parallel' // or 'serial'
});JSON Configuration
openapi-to-zod-playwright.config.json:
{
"defaults": {
"mode": "strict",
"includeDescriptions": true,
"showStats": false
},
"specs": [
{
"input": "specs/api-v1.yaml",
"output": "src/generated/api-v1.ts"
},
{
"input": "specs/api-v2.yaml",
"output": "src/generated/api-v2.ts",
"mode": "normal",
"prefix": "v2"
}
],
"executionMode": "parallel"
}Configuration Options
Playwright-Specific Options
| Option | Type | Description | Default |
|--------|------|-------------|---------|
| outputClient | string | Optional path for client class file | undefined |
| outputService | string | Optional path for service class file (requires outputClient) | undefined |
| validateServiceRequest | boolean | Enable Zod validation for request bodies in service methods | false |
| stripPathPrefix | string | Strip prefix from paths before generating method names using glob patterns (literal string or glob pattern) | undefined |
| ignoreHeaders | string[] | Header patterns to ignore (supports glob patterns like "X-*", "*") | undefined |
| basePath | string | Base path to prepend to all endpoints (e.g., "/api/v1") | undefined |
| useOperationId | boolean | Use operationId from spec for method names | true |
| operationFilters | object | Filter operations (see below) | undefined |
Operation Filters
| Filter | Type | Description |
|--------|------|-------------|
| includeTags | string[] | Include only operations with these tags |
| excludeTags | string[] | Exclude operations with these tags |
| includePaths | string[] | Include only these paths (supports glob patterns) |
| excludePaths | string[] | Exclude these paths (supports glob patterns) |
| includeMethods | string[] | Include only these HTTP methods |
| excludeMethods | string[] | Exclude these HTTP methods |
| includeOperationIds | string[] | Include only these operationIds |
| excludeOperationIds | string[] | Exclude these operationIds |
| includeDeprecated | boolean | Include deprecated operations |
| includeStatusCodes | string[] | Include only these status codes (e.g., ["2xx", "404"]) |
| excludeStatusCodes | string[] | Exclude these status codes (e.g., ["5xx"]) |
Core Generator Options
For schema generation options inherited from @cerios/openapi-to-zod (like mode, includeDescriptions, prefix, suffix, etc.), see the @cerios/openapi-to-zod Configuration.
Common inherited options:
mode:"strict"|"normal"|"loose"- Validation strictnessincludeDescriptions: Include JSDoc comments in generated schemasshowStats: Include generation statistics in outputprefix/suffix: Add prefixes/suffixes to schema names
Response Handling
Success Responses
// 200 OK with body
const users = await service.getUsers(); // Promise<User[]>
// 201 Created with body
const user = await service.postUsers201({ data: userData }); // Promise<User>
// 204 No Content
const result = await service.deleteUsersByUserId(userId); // Promise<null>Error Responses
// Test error scenarios without validation
const response = await service.getUsersByUserIdError('invalid-id');
expect(response.status()).toBe(404);
const errorBody = await response.json();
expect(errorBody.message).toContain('not found');Path Parameters
Path parameters are extracted and become required method arguments in path order:
paths:
/orgs/{orgId}/repos/{repoId}:
get:
# ...Generated:
// Path params in order
async getOrgsByOrgIdReposByRepoId(
orgId: string,
repoId: string,
options?: { query?: Record<string, any> }
): Promise<Repo>Request Options
All optional request properties are grouped in an options parameter:
interface RequestOptions {
query?: Record<string, any>; // Query parameters
headers?: Record<string, string>; // Request headers
data?: T; // Request body (JSON)
form?: Record<string, any>; // Form data
multipart?: Record<string, any>; // Multipart form data
}Service Layer (Validated)
await service.postUsers201({
data: { email: '[email protected]', name: 'Test User' }, // Validated with Zod
headers: { 'X-Custom': 'value' }
});Client Layer (Passthrough)
// All properties are Partial - allows invalid data
await client.postUsers({
data: { invalid: 'data' } // No validation
});Testing Patterns
Happy Path Testing
test('successful user creation', async ({ request }) => {
const service = new ApiService(new ApiClient(request));
const user = await service.postUsers201({
data: { email: '[email protected]', name: 'Test' }
});
// Automatically validated: status 201, response matches User schema
expect(user.id).toBeTruthy();
});Error Testing
test('handle validation errors', async ({ request }) => {
const service = new ApiService(new ApiClient(request));
const response = await service.postUsersError({
data: { email: 'invalid' } // Missing required 'name'
});
expect(response.status()).toBe(400);
const error = await response.json();
expect(error.message).toContain('validation');
});Invalid Request Testing
test('send invalid data', async ({ request }) => {
const client = new ApiClient(request);
// Use client directly to bypass validation
const response = await client.postUsers({
data: { completely: 'wrong' }
});
expect(response.status()).toBe(400);
});Requirements
- Node.js >= 16
- @playwright/test >= 1.40.0
- Zod >= 4.0.0
License
MIT © Ronald Veth - Cerios
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
For issues and questions, please use the GitHub issues page.
