DTO Generation
Generate type-safe TypeScript clients and NestJS DTOs from your OpenAPI specifications. This enables shared contracts between services with full validation and documentation support.
What Gets Generated
From a single OpenAPI spec, the generator produces two outputs:
Quick Start
Generate a Client
From your service directory:
Or using the CLI directly:
# Auto-detect service from current directory
npx tsdevstack generate-client
# Specify service name
npx tsdevstack generate-client auth-service
# Custom OpenAPI path
npx tsdevstack generate-client auth-service --input ./custom/path.json
The command auto-derives everything from the service:
- Output →
packages/{service-name}-client/
- Package name →
@shared/{service-name}-client
- Author → from service's
package.json
It then:
- Generates the HTTP client and TypeScript interfaces
- Generates DTO classes with validation decorators
- Builds the package automatically
Use the Generated Client
In a frontend application:
import { Api } from '@shared/auth-service-client';
import type { UserDto, PlanDto } from '@shared/auth-service-client';
// Create client with base URL
const client = new Api({ baseURL: process.env.NEXT_PUBLIC_API_URL });
// Public endpoints (no auth)
const plans = await client.v1.getPlans();
// Authenticated endpoints (pass JWT token)
const user: UserDto = await client.v1.getUserAccount({
headers: { Authorization: `Bearer ${accessToken}` }
});
In a NestJS service (BFF or backend):
import { UserDto } from '@shared/auth-service-client/dto';
@Controller('v1/users')
export class UsersController {
@Get()
@ApiResponse({ status: 200, type: UserDto })
async get(): Promise<UserDto> {
return { /* ... */ };
}
}
Writing DTOs That Generate Well
Follow these patterns in your source DTOs to ensure clean client generation.
Always Specify Explicit Types
Every @ApiProperty decorator must include an explicit type specification:
import { ApiProperty } from '@nestjs/swagger';
export class UserDto {
@ApiProperty({
description: 'User ID',
example: 'usr_123',
type: String, // Required
})
id: string;
@ApiProperty({
description: 'User age',
example: 25,
type: Number,
})
age: number;
@ApiProperty({
description: 'Created at',
example: '2024-01-15T10:30:00Z',
type: Date,
})
createdAt: Date;
}
Use Lazy Resolvers for Nested Objects
Prevent circular dependencies by using arrow functions:
import { PlanDto } from './plan.dto';
export class UserPlanDto {
@ApiProperty({
description: 'Plan details',
type: () => PlanDto, // Arrow function prevents circular deps
})
plan: PlanDto;
}
Arrays
@ApiProperty({
description: 'List of tags',
type: [String],
example: ['tag1', 'tag2'],
})
tags: string[];
@ApiProperty({
description: 'List of plans',
type: () => [PlanDto], // Lazy resolver for object arrays
})
plans: PlanDto[];
Enums and Nullable Properties
@ApiProperty({
description: 'System role of the user',
enum: ['USER', 'ADMIN'],
example: 'USER',
type: String,
})
systemRole: 'USER' | 'ADMIN';
@ApiProperty({
description: 'Optional notes',
example: 'VIP customer',
nullable: true,
type: String,
})
notes: string | null;
Generated Output
The generated DTO classes include validation decorators automatically:
// Generated from your OpenAPI spec
export class UserDto {
@IsDefined()
@IsString()
@ApiProperty({ required: true })
id: string;
@IsOptional()
@IsString()
@ApiProperty({ required: false, nullable: true })
notes?: string | null;
}
Date Handling
Dates in source DTOs become strings in generated clients. This is correct because JSON serializes dates as ISO 8601 strings:
// Source DTO
@ApiProperty({ type: Date })
createdAt: Date;
// Generated client DTO
createdAt: string; // Reflects actual HTTP contract
Common Issues and Solutions
Circular Dependency Errors
Error:
Error: A circular dependency has been detected (property key: "someProperty")
Solution: Add explicit type specifications to all @ApiProperty decorators, and use lazy resolvers for nested objects:
// Add type: String, type: Number, etc. to all properties
@ApiProperty({ type: String })
id: string;
// Use arrow functions for nested objects
@ApiProperty({ type: () => OtherDto })
other: OtherDto;
ConfigService Undefined During Generation
Error:
TypeError: Cannot read properties of undefined (reading 'get')
Solution: Use defensive coding in service constructors:
constructor(private config: ConfigService) {
// Use optional chaining for doc generation compatibility
this.value = this.config?.get<string>('KEY', 'default') ?? 'default';
}
Import Errors for Generated DTOs
Error:
Cannot find module '@shared/my-client/dto'
Checklist:
- Verify the package is built:
ls packages/auth-service-client/dist/dto/
- Check
package.json has the /dto export configured
- Run
npm install in your consuming project
Generated DTOs Are Interfaces Instead of Classes
This indicates a template configuration issue. The generator should produce classes with decorators. Contact the platform team if you encounter this.
Package Structure
After generation, your client package will have this structure:
packages/auth-service-client/
src/
Api.ts # HTTP client with typed methods
index.ts # Barrel export
dto/
data-contracts.ts # DTO classes with decorators
dist/ # Built output
package.json
tsconfig.json
Dependencies
Generated clients have minimal dependencies:
{
"dependencies": {
"axios": "^1.7.9"
},
"peerDependencies": {
"@nestjs/swagger": "^7.4.2",
"class-validator": "^0.14.1"
}
}
Peer dependencies mean:
- Frontend apps do not need to install NestJS packages
- Backend services use their existing NestJS installation
- No duplicate dependencies in your bundle