Uecko_ERP/docs/DTOS - GUÍA DE DISEÑO.md

412 lines
11 KiB
Markdown
Raw Normal View History

2026-04-20 17:20:42 +00:00
# Guía de diseño de DTOs para el ERP
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 1. Principios generales
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
- Un DTO representa únicamente el contrato de transporte entre cliente y servidor.
- Cada endpoint debe tener sus DTOs de `request` y `response`, aunque internamente reutilice subesquemas.
- No reutilizar el mismo DTO entre endpoints salvo que el contrato sea realmente idéntico y estable.
- Siempre que sea posible, definir DTOs a partir de esquemas Zod v4 e inferir el tipo desde el esquema.
2026-04-16 17:21:05 +00:00
```ts
export type ListIssuedInvoicesRequestDTO = z.infer<typeof ListIssuedInvoicesRequestSchema>;
```
2026-04-20 17:20:42 +00:00
- El esquema valida; no debe “arreglar” entradas. Evitar `.default()`, `.transform()` y similares salvo excepción justificada.
- No mezclar DTOs con lógica de negocio, mapping ni estado de formulario.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 2. Organización y naming
2026-04-16 17:21:05 +00:00
```plaintext
common/dto/
request/
create-proforma.request.dto.ts
get-proforma-by-id.request.dto.ts
list-proformas.request.dto.ts
update-proforma-by-id.request.dto.ts
...
response/
create-proforma.response.dto.ts
get-proforma-by-id.response.dto.ts
list-proformas.response.dto.ts
update-proforma-by-id.response.dto.ts
...
shared/
proforma-summary.dto.ts
proforma-detail.dto.ts
proforma-item.dto.ts
proforma-tax.dto.ts
recipient.dto.ts
```
2026-04-20 17:20:42 +00:00
* Estructurar DTOs por módulo en `request/`, `response/` y `shared/`.
* `shared/` solo para subesquemas reutilizables y estables.
* Nombres de fichero en `kebab-case`.
* Patrón de nombres:
2026-04-16 17:21:05 +00:00
```plaintext
<action>-<resource>[...].request.dto.ts
<action>-<resource>[...].response.dto.ts
```
2026-04-20 17:20:42 +00:00
* Usar singular/plural de forma consistente:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* colección -> plural
* elemento -> singular
2026-04-16 17:21:05 +00:00
Ejemplos:
2026-04-20 17:20:42 +00:00
```plaintext
list-proformas.request.dto.ts
get-proforma-by-id.response.dto.ts
update-proforma-by-id.request.dto.ts
2026-04-16 17:21:05 +00:00
```
2026-04-20 17:20:42 +00:00
* `<resource>-summary.dto.ts` para vistas de resumen reutilizables de list.
* `<resource>-detail.dto.ts` para vistas de detalle reutilizables de get.
* `<action>-<resource>.response.dto.ts` para la response completa del endpoint.
* `<resource>-summary.snapshot-builder.ts` para builders que materializan un item/resumen.
* Evitar nombres como `I<Resource>Snapshot` si representan el mismo contrato que un DTO ya definido por schema.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 3. Reglas de tipado
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* No usar `z.string()` si el tipo semántico del campo puede concretarse mejor.
* El mismo campo debe conservar siempre el mismo nombre y el mismo tipo en todos los DTOs.
* Ejemplos de tipado semántico:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* booleano -> `z.boolean()`
* UUID -> `z.uuid()`
* conjunto cerrado -> `z.enum([...])`
* fecha/timestamp ISO -> schema específico
* arrays -> `z.array(...)`
* objetos -> `z.object(...)`
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 4. Reglas para primitivas de negocio
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Si un identificador es UUID, usar siempre `z.uuid()`.
* Si un identificador no es UUID, definir un schema específico con nombre explícito.
* Todo estado de negocio cerrado debe modelarse con `z.enum(...)`.
* No usar `z.string()` para booleanos ni para estados cerrados.
* Distinguir explícitamente entre fecha, hora y timestamp mediante schemas específicos.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 5. Fechas, porcentajes, importes, cantidades
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
### 5.1. Amount, Money, Percentage y Quantity
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Los valores exactos no deben viajar como `number`.
* Deben viajar siempre con una única forma de transporte para toda la API.
2026-04-16 17:21:05 +00:00
```ts
type MoneyDTO = {
value: string;
scale: string;
currency_code: string;
};
type AmountDTO = {
value: string;
scale: string;
};
type PercentageDTO = {
value: string;
scale: string;
}
type QuantityDTO = {
value: string;
scale: string;
}
```
2026-04-20 17:20:42 +00:00
### 5.2. Fechas y timestamps
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Distinguir entre:
* fecha de calendario sin hora -> "2020-01-01"
* timestamp con fecha y hora ->
2026-04-16 17:21:05 +00:00
```ts
2026-04-20 17:20:42 +00:00
export const IsoDateSchema = z.iso.date(); // "2020-01-01"
export const IsoTimeSchema = z.iso.time(); // "03:15:00", "03:15"
export const OffsetDateTimeSchema = z.iso.datetime({ offset: true }); // "2020-01-01T06:15:00+02:00"
export const LocalDateTimeSchema = z.iso.datetime({ local: true }); // "2020-01-01T06:15:01"
export const DateTimeSchema1 = z.iso.datetime({ precision: -1 }); // minute precision (no seconds) -> "2020-01-01T06:15Z"
2026-04-16 17:21:05 +00:00
```
2026-04-20 17:20:42 +00:00
Si se prefiere no usar `z.iso.date()`, crear schema propio.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 6. Opcionalidad, nullabilidad y ausencia
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Significados:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* `undefined` / campo omitido -> no enviado
* `null` -> enviado explícitamente sin valor
* `""` -> cadena vacía, no indica ausencia de ese campo
* `[]` -> colección vacía, no indica ausencia de esa colección
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
No cambiar estos significados salvo decisión explícita de contrato.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
### 6.1. Para requests
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Usar `optional()` para campos no obligatorios.
* Usar `nullable()` solo cuando tenga semántica de negocio, por ejemplo “borrar” o “desvincular”.
2026-04-16 17:21:05 +00:00
Ejemplo:
```ts
payment_method_id: z.uuid().nullable().optional()
2026-04-20 17:20:42 +00:00
```
2026-04-16 17:21:05 +00:00
Interpretación:
* omitido -> no tocar
2026-04-20 17:20:42 +00:00
* `null` -> quitar relación
* UUID -> asignar relación
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
### 6.2. Para responses
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Decidir de forma explícita si una ausencia semántica se representa con `null` o con omisión.
* Para relaciones opcionales, suele ser más claro usar `null` que omitir la relación.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 7. Reglas para CREATE
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* El request de `create` debe contener solo los datos necesarios para crear el recurso. El resto, como opcionales => `optional()`
2026-04-16 17:21:05 +00:00
* No reutilizar el DTO de detalle como DTO de creación.
2026-04-20 17:20:42 +00:00
* No usar defaults en requests de negocio.
* Si hace falta normalización, hacerla fuera del DTO:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
```plaintext
validación DTO => normalización input => mapeo a command/props
2026-04-16 17:21:05 +00:00
```
2026-04-20 17:20:42 +00:00
## 8. Reglas para UPDATE
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* `update` debe seguir semántica tipo `PATCH`:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* solo viajan los campos a modificar
* los campos omitidos no se tocan
* La opcionalidad debe declararse en el esquema con `optional()`.
* No usar `Partial<>` sobre tipos inferidos si el esquema ya expresa opcionalidad.
* No usar defaults en `update`, especialmente en colecciones.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Incorrecto:
2026-04-16 17:21:05 +00:00
```ts
items: z.array(...).default([])
```
Correcto:
```ts
items: z.array(...).optional()
```
2026-04-20 17:20:42 +00:00
### 8.1. UPDATE de colecciones
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Por defecto, mantener reemplazo completo de la colección.
* Si se necesita granularidad real, usar una estructura explícita:
2026-04-16 17:21:05 +00:00
```ts
items: {
2026-04-20 17:20:42 +00:00
create: [...],
update: [...],
delete: [...],
2026-04-16 17:21:05 +00:00
}
```
2026-04-20 17:20:42 +00:00
## 9. Responses de LIST y DETAIL
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* `list` y `get` no deben compartir el mismo esquema completo.
* Separar vistas de resumen y detalle:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* `List` -> summary
* `Get` -> detail
* `list` debe contener solo lo necesario para el listado.
* Puede existir un esquema base compartido solo si mejora claridad y estabilidad.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 10. Subesquemas reutilizables
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Extraer a `shared/` toda estructura no trivial que se repita y sea estable.
* No extraer abstracciones prematuramente.
Ejemplos típicos:
* `recipient`
* `item`
* `tax-breakdown`
* `payment-method-ref`
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 10.bis. Snapshot builders y materialización de DTOs
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Los snapshot builders de API existen para materializar objetos de salida serializables a partir de modelos de aplicación, agregados o read models.
* Un snapshot builder **no define el contrato HTTP**; el contrato lo define el schema DTO.
* El schema Zod es la **única fuente de verdad (SSOT)** del contrato de salida.
2026-04-16 17:21:05 +00:00
---
2026-04-20 17:20:42 +00:00
### Regla principal
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
> Si un snapshot builder genera un DTO de API, su tipo de salida debe ser el `z.infer` del schema exacto que representa.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Ejemplo:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
```ts
export const CustomerSummarySchema = z.object({ ... });
export type CustomerSummaryDTO = z.infer<typeof CustomerSummarySchema>;
export class CustomerSummarySnapshotBuilder
implements ISnapshotBuilder<CustomerSummary, CustomerSummaryDTO> {
public toOutput(model: CustomerSummary): CustomerSummaryDTO {
...
}
}
```
2026-04-16 17:21:05 +00:00
---
2026-04-20 17:20:42 +00:00
### Reglas obligatorias
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* No duplicar el contrato mediante interfaces manuales paralelas si ya existe un schema Zod equivalente.
* Eliminar tipos como `ICustomerSummarySnapshot` si representan el mismo contrato que un DTO.
* Si el builder construye una subvista reutilizable, debe tiparse contra el subschema compartido correspondiente (`shared/`).
* Si el builder construye la response completa, debe tiparse contra el response DTO completo.
* El builder debe devolver exactamente la forma del schema (no “aproximada”).
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
---
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
### Responsabilidades del snapshot builder
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Puede:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Mapear `Maybe``null`
* Convertir Value Objects → primitives o DTOs (`string`, `{ value, scale }`, etc.)
* Adaptar enums internos → valores de contrato
* Construir vistas (`summary`, `detail`, etc.)
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
No puede:
* Definir contratos
* Validar reglas de negocio
* Acceder a repositorios
* Ejecutar lógica de dominio
* “corregir” datos fuera del contrato definido
2026-04-16 17:21:05 +00:00
---
2026-04-20 17:20:42 +00:00
### Separación por niveles
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Si el endpoint es:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
```ts
ListCustomersResponseSchema
```
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Entonces:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Subschema:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
```ts
CustomerSummarySchema
```
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Builder:
```ts
CustomerSummarySnapshotBuilder -> CustomerSummaryDTO
```
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
No:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
```ts
Builder -> ListCustomersResponseDTO ❌
```
2026-04-16 17:21:05 +00:00
---
2026-04-20 17:20:42 +00:00
### Naming obligatorio
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
DTO:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
```plaintext
<resource>-summary.dto.ts
<resource>-detail.dto.ts
<action>-<resource>.response.dto.ts
```
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Builders:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
```plaintext
<resource>-summary.snapshot-builder.ts
<resource>-detail.snapshot-builder.ts
<action>-<resource>.response.snapshot-builder.ts
```
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Evitar:
```plaintext
I<Resource>Snapshot ❌
<Resource>Output ❌
<Resource>Interface ❌
```
2026-04-16 17:21:05 +00:00
---
2026-04-20 17:20:42 +00:00
### Regla de consistencia
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
> Si cambia el schema DTO, el builder debe romper en compile-time.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Esto garantiza:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* contrato consistente
* ausencia de drift
* eliminación de duplicación
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
---
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 11. Errores
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* El DTO de error debe ser estable, tipado y predecible.
* No usar shapes excesivamente genéricos si ya se conoce la estructura mínima útil.
* El cliente debe poder representar errores sin heurísticas frágiles.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 12. Metadata
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* `metadata` solo debe contener datos auxiliares, trazabilidad no crítica, versionado o hints no funcionales.
* Si un dato es necesario para que el cliente funcione, no debe ir en `metadata`, sino en el DTO principal.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 13. Evolución de la API
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* Es mejor añadir campos que reinterpretar campos existentes.
* Cambiar el significado o la estructura de un campo implica cambio de contrato.
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 14. Criterio para detectar overengineering
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Un diseño de DTO está sobreingenierizado si:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* para entender un endpoint simple hay que abrir demasiados ficheros
* el esquema valida, normaliza y transforma a la vez
* se extraen subesquemas que apenas se reutilizan
* el DTO intenta reflejar demasiado fielmente el dominio interno
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Regla práctica:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
* preferir duplicación pequeña y clara frente a abstracción prematura
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
## 15. Checklist de revisión
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
Antes de cerrar un DTO, comprobar:
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
1. ¿Representa solo contrato de transporte?
2. ¿Cada campo usa el tipo semántico más preciso posible?
3. ¿Se distingue bien entre omitido, `null`, vacío y colección vacía?
4. ¿Hay defaults ocultando intención?
5. ¿El naming es consistente?
6. ¿Las estructuras repetidas merecen extraerse?
7. ¿Hay abstracciones prematuras?
2026-04-16 17:21:05 +00:00
8. ¿El cliente puede consumirlo sin heurísticas frágiles?
2026-04-20 17:20:42 +00:00
9. ¿El mismo campo mantiene el mismo tipo en todo el módulo?
10. ¿El DTO refleja una vista concreta del endpoint?
2026-04-16 17:21:05 +00:00
2026-04-20 17:20:42 +00:00
---