.agents/skills/dto-sync-patterns/SKILL.md
Patterns for synchronizing DTOs between frontend and backend. Covers shared types, code generation, and validation sync. USE WHEN: user asks about "DTO sync", "shared types", "frontend backend types", "type consistency", "API models" DO NOT USE FOR: type generation tools - use `type-generation` skill, validation rules - use validation skills
npx skillsauth add d-subrahmanyam/deno-fresh-microservices dto-sync-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
type-generation skillopenapi-contract skill┌─────────────────────────────────────────────────────────────────────┐
│ DTO SYNC STRATEGIES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Schema-First (Recommended) │
│ ┌──────────────┐ │
│ │ OpenAPI Spec │───→ Generate Backend DTOs │
│ │ (Source) │───→ Generate Frontend Types │
│ └──────────────┘ │
│ │
│ 2. Backend-First │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Backend DTOs │───→ │ OpenAPI Spec │───→ Frontend Types │
│ │ (Source) │ │ (Generated) │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 3. Shared Package (Monorepo) │
│ ┌──────────────┐ │
│ │ @shared/types│───→ Backend imports │
│ │ (TypeScript) │───→ Frontend imports │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
# openapi.yaml - Single source of truth
components:
schemas:
CreateUserRequest:
type: object
required:
- email
- name
properties:
email:
type: string
format: email
maxLength: 255
name:
type: string
minLength: 2
maxLength: 100
age:
type: integer
minimum: 0
maximum: 150
User:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
name:
type: string
age:
type: integer
createdAt:
type: string
format: date-time
# Generate Java DTOs from OpenAPI
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g spring \
-o generated/java \
--additional-properties=useJakartaEe=true
// Generated: CreateUserRequest.java
@Generated
public class CreateUserRequest {
@NotNull
@Email
@Size(max = 255)
private String email;
@NotNull
@Size(min = 2, max = 100)
private String name;
@Min(0)
@Max(150)
private Integer age;
// getters, setters...
}
# Generate TypeScript types from OpenAPI
npx openapi-typescript openapi.yaml -o src/api/types.ts
// Generated: types.ts
export interface components {
schemas: {
CreateUserRequest: {
email: string;
name: string;
age?: number;
};
User: {
id?: string;
email?: string;
name?: string;
age?: number;
createdAt?: string;
};
};
}
// Spring Boot with springdoc-openapi
@Schema(description = "Request to create a new user")
public record CreateUserRequest(
@Schema(description = "User email", example = "[email protected]")
@NotNull
@Email
@Size(max = 255)
String email,
@Schema(description = "User name", example = "John Doe")
@NotNull
@Size(min = 2, max = 100)
String name,
@Schema(description = "User age", minimum = "0", maximum = "150")
@Min(0)
@Max(150)
Integer age
) {}
# Export OpenAPI spec from running backend
curl http://localhost:8080/v3/api-docs > openapi.json
# Generate frontend types
npx openapi-typescript openapi.json -o src/api/types.ts
// NestJS DTO with decorators
@Schema({ description: 'Request to create a new user' })
export class CreateUserDto {
@ApiProperty({ example: '[email protected]' })
@IsEmail()
@MaxLength(255)
email: string;
@ApiProperty({ example: 'John Doe' })
@IsString()
@Length(2, 100)
name: string;
@ApiPropertyOptional({ minimum: 0, maximum: 150 })
@IsOptional()
@IsInt()
@Min(0)
@Max(150)
age?: number;
}
# Export from NestJS
# (requires @nestjs/swagger setup)
curl http://localhost:3000/api-json > openapi.json
monorepo/
├── packages/
│ ├── shared/
│ │ ├── package.json
│ │ └── src/
│ │ ├── types/
│ │ │ ├── user.ts
│ │ │ └── index.ts
│ │ └── validation/
│ │ ├── user.ts
│ │ └── index.ts
│ ├── frontend/
│ │ └── package.json # depends on @shared
│ └── backend/
│ └── package.json # depends on @shared
└── package.json
// packages/shared/src/types/user.ts
export interface User {
id: string;
email: string;
name: string;
age?: number;
createdAt: Date;
}
export interface CreateUserRequest {
email: string;
name: string;
age?: number;
}
export interface UpdateUserRequest {
name?: string;
age?: number;
}
// Type guards
export function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'email' in obj
);
}
// packages/shared/src/validation/user.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(2).max(100),
age: z.number().int().min(0).max(150).optional(),
});
export const UpdateUserSchema = z.object({
name: z.string().min(2).max(100).optional(),
age: z.number().int().min(0).max(150).optional(),
});
// Infer types from schemas
export type CreateUserRequest = z.infer<typeof CreateUserSchema>;
export type UpdateUserRequest = z.infer<typeof UpdateUserSchema>;
// packages/frontend/src/api/users.ts
import type { User, CreateUserRequest } from '@shared/types';
import { CreateUserSchema } from '@shared/validation';
async function createUser(data: CreateUserRequest): Promise<User> {
// Validate before sending
const validated = CreateUserSchema.parse(data);
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(validated),
});
return response.json();
}
// packages/backend/src/routes/users.ts
import type { CreateUserRequest } from '@shared/types';
import { CreateUserSchema } from '@shared/validation';
app.post('/api/users', async (req, res) => {
// Same validation as frontend
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
code: 'VALIDATION_ERROR',
details: result.error.issues,
});
}
const user = await userService.create(result.data);
res.json(user);
});
// Shared schema
const UserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
});
// Frontend: Form validation
const form = useForm({
resolver: zodResolver(UserSchema),
});
// Backend: Request validation
app.post('/users', (req, res) => {
const result = UserSchema.safeParse(req.body);
});
// Backend: class-validator
class CreateUserDto {
@IsEmail()
@MaxLength(255)
email: string;
@IsString()
@Length(2, 100)
name: string;
}
// Frontend: Equivalent Zod schema
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(2).max(100),
});
// Backend: Jakarta validation
public record CreateUserRequest(
@NotNull @Email @Size(max = 255) String email,
@NotNull @Size(min = 2, max = 100) String name
) {}
// Frontend: Equivalent Zod
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(2).max(100),
});
// Frontend form data → API request
interface FormData {
firstName: string;
lastName: string;
birthDate: Date;
}
interface CreateUserRequest {
name: string; // Concatenated
age: number; // Calculated
}
function toCreateUserRequest(form: FormData): CreateUserRequest {
const age = calculateAge(form.birthDate);
return {
name: `${form.firstName} ${form.lastName}`,
age,
};
}
// API response → Frontend model
interface UserResponse {
id: string;
created_at: string; // snake_case
full_name: string;
}
interface User {
id: string;
createdAt: Date; // camelCase
fullName: string;
}
function toUser(response: UserResponse): User {
return {
id: response.id,
createdAt: new Date(response.created_at),
fullName: response.full_name,
};
}
import camelcaseKeys from 'camelcase-keys';
import snakecaseKeys from 'snakecase-keys';
// Axios interceptor
axios.interceptors.request.use((config) => {
if (config.data) {
config.data = snakecaseKeys(config.data, { deep: true });
}
return config;
});
axios.interceptors.response.use((response) => {
if (response.data) {
response.data = camelcaseKeys(response.data, { deep: true });
}
return response;
});
## DTO Sync Validation Report
### CreateUserRequest
| Field | Backend | Frontend | Status |
|-------|---------|----------|--------|
| email | @Email @Size(max=255) | z.string().email().max(255) | OK |
| name | @Size(min=2, max=100) | z.string().min(2).max(100) | OK |
| age | @Min(0) @Max(150) | z.number().min(0).max(150) | OK |
### User Response
| Field | Backend | Frontend | Status |
|-------|---------|----------|--------|
| id | UUID | string | OK |
| createdAt | Instant | Date | OK (transformed) |
| name | String | string | OK |
### Recommendations
1. All validations are in sync
2. Date transformation handled in response interceptor
| Anti-Pattern | Why It's Bad | Correct Approach | |--------------|--------------|------------------| | Manual type copying | Drift over time | Generate from schema | | Different validation rules | Inconsistent errors | Share validation logic | | No transformation layer | Tight coupling | Add DTOs for each layer | | Ignoring optionality | Runtime errors | Match required/optional exactly | | snake_case/camelCase mismatch | Confusion | Auto-transform consistently |
| Issue | Likely Cause | Solution | |-------|--------------|----------| | Type mismatch | Manual sync drift | Regenerate from schema | | Validation passes frontend, fails backend | Different rules | Align validation schemas | | Missing required field | Optionality mismatch | Check OpenAPI required array | | Date parsing error | String vs Date | Add transformation | | Case mismatch | snake_case vs camelCase | Add case conversion |
development
Guidelines for building high-performance APIs with Fastify and TypeScript, covering validation, Prisma integration, and testing best practices
development
FastAPI modern Python web framework. Covers routing, Pydantic models, dependency injection, and async support. Use when building Python APIs. USE WHEN: user mentions "fastapi", "pydantic", "async python api", "python rest api", asks about "dependency injection python", "python openapi", "python swagger", "async endpoints", "python api validation", "fastapi middleware" DO NOT USE FOR: Django apps - use `django` instead, Flask apps - use `flask` instead, synchronous Python APIs without type hints, GraphQL-only APIs
tools
FastAPI integration testing specialist. Covers synchronous TestClient, async httpx AsyncClient, dependency injection overrides, auth testing (JWT, OAuth2, API keys), WebSocket testing, file uploads, background tasks, middleware testing, and HTTP mocking with respx, responses, and pytest-httpserver. USE WHEN: user mentions "FastAPI test", "TestClient", "httpx async test", "dependency override test", "respx mock", asks about testing FastAPI endpoints, authentication in tests, or HTTP client mocking. DO NOT USE FOR: Django - use `pytest-django`; pytest internals - use `pytest`; Container infrastructure - use `testcontainers-python`
development
Expert in FastAPI Python development with best practices for APIs and async operations