---
title: Use DTOs at Layer Boundaries
impact: HIGH
impactDescription: Prevents internal data structures from leaking across layers
tags: data, dto, boundaries, validation, zod
---
## Паспорт документа

- Статус документа: living standard
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении инженерной практики, CI/CD, архитектурных правил или локального workflow
- Область применения: внутренние rule/reference-card документы для инженерной команды
- Связанные документы:
  - [Индекс Agents](../README.md)
  - [Команды разработки](../commands.md)
  - [Инженерные принципы](../../governance/engineering-principles.md)

## Use DTOs at Layer Boundaries

**Impact: HIGH**

Data Transfer Objects (DTOs) define the shape of data at each boundary:
- **Input DTOs**: Validate and type incoming data (Zod schemas)
- **Response DTOs**: Shape outgoing data (no sensitive fields)

**Incorrect (raw Prisma types leaking to API):**

```typescript
@Get(':id')
async getItem(@Param('id') id: string) {
  return this.prisma.item.findFirst({ where: { id } }); // Exposes all fields!
}
```

**Correct (DTO at each boundary):**

```typescript
// packages/shared/src/schemas/item.ts — shared Zod schema
import { z } from 'zod';

export const CreateItemSchema = z.object({
  name: z.string().min(3).max(200),
  description: z.string().min(10).max(5000).optional(),
  shortDescription: z.string().max(300).optional(),
  priceFrom: z.number().nonnegative().optional(),
  priceTo: z.number().nonnegative().optional(),
  subjectId: z.string().uuid().optional(),
  studyFormat: z.enum(['ONLINE', 'OFFLINE', 'HYBRID']),
  studyType: z.enum(['GROUP', 'MINI_GROUP', 'ONE_ON_ONE']).optional(),
  language: z.enum(['RU', 'UZ_LATIN', 'UZ_CYRILLIC', 'EN', 'KK', 'TG', 'ANY']).default('ANY'),
});

export type CreateItemDTO = z.infer<typeof CreateItemSchema>;

// apps/api/src/modules/item/dto/item-response.dto.ts
export interface ItemListDTO {
  id: string;
  name: string;
  slug: string;
  priceFrom: number | null;
  priceTo: number | null;
  imageUrl: string | null;
  studyFormat: string;
  seller: { id: string; type: string };
  location: { name: string } | null;
  ratingAvg: number;
  reviewsCount: number;
}

// Controller uses the response DTO type
@Get(':slug')
async getBySlug(@Param('slug') slug: string): Promise<ItemDetailDTO> {
  const item = await this.itemService.findBySlug(slug);
  if (!item) throw new NotFoundException();
  return item; // Already shaped by repository's select
}
```

**Rules:**
- Input validation schemas live in `packages/shared` (shared with frontend)
- Response DTOs are defined per use-case (list vs detail vs admin)
- Repositories shape data via Prisma `select` to match DTOs
- Never return raw Prisma objects from controllers
