Skip to main content

Automated OpenAPI Generation with Zod: Eliminating Manual Schema Maintenance

Β· 8 min read
Mike
DAGGH Lead Developer

We've completely revolutionized our API documentation workflow at DAGGH by implementing a fully automated OpenAPI generation system powered by Zod schemas. This post dives deep into our technical implementation, design decisions, and the dramatic improvements in developer experience.

The Problem: Dual Schema Maintenance Hell​

Previously, our API documentation workflow suffered from a classic problem: dual schema maintenance. We had to maintain schemas in two places:

  1. Zod schemas for runtime validation and TypeScript types
  2. Manual OpenAPI schemas for API documentation

This created several pain points:

  • πŸ”„ Manual synchronization between Zod and OpenAPI schemas
  • πŸ› Schema drift when one was updated but not the other
  • ⏰ Time-consuming maintenance for every API change
  • ❌ Inconsistent validation between runtime and documentation
  • πŸ“ Poor documentation quality due to maintenance overhead

The Solution: Zod as Single Source of Truth​

We implemented a revolutionary approach: Zod schemas as the single source of truth with fully automated OpenAPI generation.

Architecture Overview​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Zod Schemas β”‚ -> β”‚ OpenAPI Generatorβ”‚ -> β”‚ Documentation β”‚
β”‚ (Manual) β”‚ β”‚ (Automated) β”‚ β”‚ (Automated) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Technologies​

  • Zod - Schema validation and TypeScript type generation
  • @asteasolutions/zod-to-openapi - Zod to OpenAPI conversion
  • Custom generator - Route scanning and spec assembly
  • swagger-jsdoc - JSDoc path extraction from route files

Technical Implementation​

1. Comprehensive Zod Schemas with OpenAPI Metadata​

Our schemas in src/schemas/api-schemas.ts combine runtime validation with rich OpenAPI documentation:

export const MovieSchema = z
.object({
tmdb_id: z.number().int().describe("TMDB movie identifier"),
title: z.string().describe("Movie title"),
overview: z.string().optional().describe("Movie synopsis"),
release_date: z.string().optional().describe("Release date in YYYY-MM-DD format"),
vote_average: z.number().optional().describe("Average user rating (0-10)"),
genres: z.array(GenreSchema).optional().describe("Movie genres"),
})
.openapi({
ref: "Movie",
description: "Movie information",
example: {
tmdb_id: 550,
title: "Fight Club",
overview: "A ticking-time-bomb insomniac and a slippery soap salesman...",
release_date: "1999-10-15",
vote_average: 8.4,
genres: [
{ id: 18, name: "Drama" },
{ id: 53, name: "Thriller" },
],
},
})

Key features:

  • Rich descriptions using .describe() for every field
  • OpenAPI metadata with .openapi() including examples
  • Validation rules (min/max, optional, arrays, etc.)
  • Nested schemas for complex data structures

2. Automated Generator Architecture​

Our production generator (openapi/generate-production.js) follows a sophisticated multi-stage process:

Stage 1: Schema Registration​

// Auto-register all Zod schemas with OpenAPI metadata
Object.entries(schemas).forEach(([name, schema]) => {
try {
registry.register(name, schema)
console.log(` βœ… ${name}`)
} catch (error) {
console.log(` ❌ ${name}: ${error.message}`)
}
})

Stage 2: Component Generation​

// Convert registered schemas to OpenAPI components
const generator = new OpenApiGeneratorV3(registry.definitions)
const components = generator.generateComponents()

Stage 3: Route Scanning​

// Scan API routes for JSDoc path definitions
const apiFiles = glob.sync("./src/app/api/**/*.ts")
const specs = swaggerJsdoc({
definition: baseDefinition,
apis: apiFiles,
})

Stage 4: Spec Assembly​

// Merge components and paths into final specification
const finalSpec = {
...baseDefinition,
components: {
...baseDefinition.components,
schemas: components.components.schemas,
},
paths: specs.paths || {},
}

3. Route Integration Pattern​

API routes reference auto-generated schemas using standard OpenAPI $ref patterns:

/**
* @swagger
* /api/movies/random:
* get:
* summary: Get random movies for swiping
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* minimum: 1
* maximum: 50
* default: 20
* responses:
* 200:
* description: Random movies retrieved successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/RandomMoviesResponse'
* 400:
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
export async function GET(request: NextRequest) {
// Implementation using the same Zod schema for validation
const result = RandomMoviesResponseSchema.parse(responseData)
return NextResponse.json(result)
}

Design Decisions & Rationale​

Why Zod as Source of Truth?​

  1. Runtime Validation: Zod provides actual runtime validation, not just documentation
  2. TypeScript Integration: Automatic type generation from schemas
  3. Rich Validation: Complex validation rules (regex, custom validators, transforms)
  4. Ecosystem: Excellent tooling and community support
  5. Maintainability: Single place to define data contracts

Why Not TypeScript-First?​

We evaluated generating schemas from TypeScript types but chose Zod because:

  • Validation Logic: TypeScript types don't provide runtime validation
  • OpenAPI Features: Limited support for examples, descriptions, constraints
  • Complex Types: Difficulty handling unions, conditionals, transforms
  • Ecosystem Maturity: Zod-to-OpenAPI tooling is more mature

Why Custom Generator vs. Off-the-Shelf?​

Our custom generator provides:

  • Route Integration: Seamless JSDoc path extraction from Next.js routes
  • DAGGH-Specific Features: Custom metadata, examples, validation patterns
  • Error Handling: Robust error reporting and validation
  • Flexibility: Easy to extend for future requirements

Workflow & Developer Experience​

The New Development Flow​

  1. Define/Modify API β†’ Update Zod schema in src/schemas/api-schemas.ts
  2. Add Route JSDoc β†’ Document endpoint with standard JSDoc annotations
  3. Reference Schema β†’ Use $ref to auto-generated schema components
  4. Generate Docs β†’ Run pnpm docs:update
  5. Deploy β†’ Documentation automatically updated

Available Scripts​

# Generate OpenAPI spec from Zod schemas
pnpm openapi:generate

# Validate the generated specification
pnpm openapi:validate

# Check route documentation coverage
pnpm openapi:check

# Copy spec to documentation site
pnpm openapi:copy

# Complete workflow: check β†’ generate β†’ copy
pnpm docs:update

# Full documentation site generation
pnpm docs:full

Developer Benefits​

  • βœ… Zero Manual Schema Maintenance - Schemas auto-generated from Zod
  • βœ… Type Safety Everywhere - Same schemas for validation, types, and docs
  • βœ… Rich Documentation - Detailed descriptions, examples, validation rules
  • βœ… Fast Iteration - Single command updates everything
  • βœ… Consistency Guaranteed - Impossible for docs to drift from implementation
  • βœ… Better DX - Focus on business logic, not schema maintenance

Results & Impact​

Before vs. After​

AspectBefore (Manual)After (Automated)
Schema MaintenanceManual dual maintenanceZero maintenance
Documentation QualityBasic, often outdatedRich with examples
ConsistencyProne to driftGuaranteed consistency
Development TimeHigh overheadMinimal overhead
Error ProneManual sync errorsAutomated validation
Type SafetyPartialComplete

Metrics​

  • 21 API endpoints fully documented
  • 7 comprehensive schemas auto-generated
  • 100% validation coverage - zero OpenAPI validation errors
  • 53KB comprehensive OpenAPI specification
  • ~90% reduction in documentation maintenance time

Code Quality Improvements​

// Before: Separate validation and documentation
const movieData = req.body // No validation
// ... manual OpenAPI schema in separate file

// After: Single source of truth
const movieData = CreateMovieRequestSchema.parse(req.body) // Runtime validation
// OpenAPI schema automatically generated from same Zod schema

Advanced Features​

Complex Schema Relationships​

Our implementation handles sophisticated schema relationships:

export const CommentsResponseSchema = z
.object({
comments: z.array(CommentSchema).describe("Array of comments"),
total: z.number().int().describe("Total number of comments"),
page: z.number().int().describe("Current page number"),
pageSize: z.number().int().describe("Number of comments per page"),
})
.openapi({
ref: "CommentsResponse",
description: "Response for comments list endpoint",
})

Rich Validation Rules​

Zod's powerful validation automatically becomes OpenAPI constraints:

export const SyncMoviesRequestSchema = z
.object({
movieIds: z.array(z.number().int()).min(1).describe("Array of TMDB movie IDs"),
force: z.boolean().default(false).describe("Force re-sync even if movie exists"),
includeCredits: z.boolean().default(true).describe("Include cast and crew"),
})
.openapi({
ref: "SyncMoviesRequest",
description: "Request to sync movies from TMDB",
})

This generates OpenAPI with:

  • Array validation (minItems: 1)
  • Boolean defaults
  • Rich descriptions
  • Type constraints

Error Handling & Validation​

The generator includes comprehensive error handling:

// Schema registration with error recovery
Object.entries(schemas).forEach(([name, schema]) => {
try {
registry.register(name, schema)
registeredCount++
} catch (error) {
console.log(` ❌ ${name}: ${error.message}`)
errors.push({ schema: name, error: error.message })
}
})

// Validation of final specification
await swaggerParser.validate(finalSpec)
console.log("βœ… OpenAPI specification is valid!")

Future Enhancements​

Planned Improvements​

  1. Runtime Validation Integration - Automatic Zod validation in API routes
  2. Enhanced Examples - Auto-generated examples from factory functions
  3. Schema Versioning - Support for API versioning through schema evolution
  4. Performance Optimization - Incremental generation for large schema sets
  5. Testing Integration - Auto-generated API tests from schemas

Extensibility​

The architecture is designed for easy extension:

// Custom Zod extensions for DAGGH-specific features
export const DAGGHMovieSchema = MovieSchema.extend({
user_rating: z.number().min(1).max(10).optional(),
swipe_status: z.enum(["liked", "disliked", "unseen"]).optional(),
}).openapi({
ref: "DAGGHMovie",
description: "Movie with user-specific data",
})

Migration Guide​

For teams looking to implement similar automation:

1. Assessment Phase​

  • Audit existing schema definitions
  • Identify validation vs. documentation gaps
  • Evaluate Zod compatibility

2. Implementation Phase​

  • Define comprehensive Zod schemas with OpenAPI metadata
  • Build or adapt generation pipeline
  • Create validation and testing workflows

3. Migration Phase​

  • Parallel implementation with legacy system
  • Route-by-route migration with validation
  • Legacy system deprecation

4. Optimization Phase​

  • Performance tuning
  • Developer experience improvements
  • Advanced feature implementation

Conclusion​

Our automated OpenAPI generation system represents a significant leap forward in API documentation and developer experience. By choosing Zod as our single source of truth, we've eliminated the pain of dual schema maintenance while gaining superior type safety, validation, and documentation quality.

The key insight is that documentation and validation should come from the same source. When your schemas serve both runtime validation and documentation generation, consistency is guaranteed, and developer productivity soars.

For developers working on API-heavy applications, this approach provides a template for eliminating one of the most persistent pain points in modern web development: keeping documentation in sync with implementation.

The result? More time building features, less time maintaining schemas, and documentation that developers actually trust and use.


Want to learn more about our technical implementation? Explore the source code for detailed implementation examples and see our automated OpenAPI generation in action.