dto-sync-patterns

v2026.09.24

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

GitHub
Install command
npx skhub add claude-dev-suite/dto-sync-patterns
Markdown
SKILL.md

DTO Sync Patterns - Quick Reference

When NOT to Use This Skill

  • Type generation setup - Use type-generation skill
  • Validation implementation - Use language-specific validation skills
  • API contract validation - Use openapi-contract skill

Sync Strategy Overview

┌─────────────────────────────────────────────────────────────────────┐
│                        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                           │
│     └──────────────┘                                                │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Pattern 1: Schema-First

OpenAPI as Source of Truth

# 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 for Backend (Java)

# 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 for Frontend (TypeScript)

# 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;
    };
  };
}

Pattern 2: Backend-First

Backend Generates OpenAPI

// Spring Boot with springdoc-openapi
@Schema(description = "Request to create a new user")
public record CreateUserRequest(
    @Schema(description = "User email", example = "john@example.com")
    @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 with Swagger

// NestJS DTO with decorators
@Schema({ description: 'Request to create a new user' })
export class CreateUserDto {
  @ApiProperty({ example: 'john@example.com' })
  @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

Pattern 3: Shared Package (Monorepo)

Project Structure

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

Shared Types

// 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
  );
}

Shared Validation (Zod)

// 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>;

Frontend Usage

// 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();
}

Backend Usage (Node.js)

// 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);
});

Validation Sync

Zod (TypeScript Both Ends)

// 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);
});

class-validator (NestJS) ↔ Zod (Frontend)

// 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),
});

Java Bean Validation ↔ Zod (Frontend)

// 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),
});

Transformation Patterns

Request Transformation

// 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,
  };
}

Response Transformation

// 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,
  };
}

Automatic Case Conversion

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;
});

Validation Sync Report

## 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-Patterns

Anti-PatternWhy It's BadCorrect Approach
Manual type copyingDrift over timeGenerate from schema
Different validation rulesInconsistent errorsShare validation logic
No transformation layerTight couplingAdd DTOs for each layer
Ignoring optionalityRuntime errorsMatch required/optional exactly
snake_case/camelCase mismatchConfusionAuto-transform consistently

Quick Troubleshooting

IssueLikely CauseSolution
Type mismatchManual sync driftRegenerate from schema
Validation passes frontend, fails backendDifferent rulesAlign validation schemas
Missing required fieldOptionality mismatchCheck OpenAPI required array
Date parsing errorString vs DateAdd transformation
Case mismatchsnake_case vs camelCaseAdd case conversion

Related Skills

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/integration-validation/dto-sync-patterns

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1