Executando verificação de segurança...
7

Domain-Driven Design na Prática com TypeScript e Prisma

O Prisma gera tipos a partir do schema. Esses tipos vazam para controllers, services, validações e até para o frontend. Em pouco tempo, o modelo do banco de dados vira o modelo do negócio: uma coluna renomeada quebra 40 arquivos. Esse é o sintoma de domínio acoplado à infraestrutura.

Domain-Driven Design resolve exatamente isso: isola as regras de negócio em objetos que não sabem (e não precisam saber) que o Prisma existe. O problema é que a maioria dos tutoriais de DDD em TypeScript cria 15 camadas de abstração para um CRUD de TODO app. O resultado é uma arquitetura astronauta que ninguém quer manter.

Este post aplica DDD de forma pragmática: Value Objects, Entities, Aggregates e Repositories com TypeScript e Prisma, sem inventar camadas desnecessárias, com código que compila e roda.

Onde o Prisma termina e o domínio começa

O Prisma faz duas coisas muito bem: gera tipos a partir do schema e abstrai queries SQL. O erro é usar esses tipos gerados como modelos de negócio. Quando você passa um Prisma.OrderGetPayload<{include: {items: true}}> para dentro de uma função de cálculo de desconto, está dizendo que a regra de desconto depende da estrutura do banco.

A separação mínima viável é:

CamadaResponsabilidadeConhece o Prisma?
Domain (Entities, Value Objects, Aggregates)Regras de negócio, invariantes, validaçõesNão
Application (Use Cases / Services)Orquestra fluxo, chama repositóriosNão
Infrastructure (Repositories, Controllers)Persiste, recebe HTTP, envia emailSim

Se seu projeto tem menos de 5 entidades e zero regras de negócio complexas, DDD é over-engineering. Use Prisma direto no service e siga em frente. DDD compensa quando existem invariantes reais: "pedido não pode ter valor negativo", "CPF precisa ser válido", "estoque não pode ficar abaixo de zero após reserva".

Value Objects: validação que não vaza

Value Objects são objetos imutáveis definidos pelo valor, não por identidade. CPF, Email, Money, Quantity: se dois têm o mesmo valor, são iguais. A validação mora dentro do Value Object, não espalhada em controllers e middlewares.

// src/domain/value-objects/email.ts

export class Email {
  private constructor(private readonly value: string) {}

  static create(raw: string): Email {
    const trimmed = raw.trim().toLowerCase();
    // Regex simples: validação real de email é feita por confirmação, não por regex perfeita
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
      throw new InvalidEmailError(raw);
    }
    return new Email(trimmed);
  }

  toString(): string {
    return this.value;
  }

  equals(other: Email): boolean {
    return this.value === other.value;
  }
}

export class InvalidEmailError extends Error {
  constructor(raw: string) {
    super(`Email inválido: "${raw}"`);
    this.name = "InvalidEmailError";
  }
}
// src/domain/value-objects/money.ts

export class Money {
  // Armazena em centavos para evitar floating point (R$ 10,50 = 1050)
  private constructor(private readonly cents: number) {}

  static fromCents(cents: number): Money {
    if (!Number.isInteger(cents) || cents < 0) {
      throw new Error(`Valor monetário inválido: ${cents} centavos`);
    }
    return new Money(cents);
  }

  static fromReais(reais: number): Money {
    return Money.fromCents(Math.round(reais * 100));
  }

  add(other: Money): Money {
    return Money.fromCents(this.cents + other.cents);
  }

  multiply(factor: number): Money {
    return Money.fromCents(Math.round(this.cents * factor));
  }

  isGreaterThan(other: Money): boolean {
    return this.cents > other.cents;
  }

  toCents(): number {
    return this.cents;
  }

  toReais(): number {
    return this.cents / 100;
  }

  equals(other: Money): boolean {
    return this.cents === other.cents;
  }
}

O construtor é privado. A única forma de criar um Email ou Money é passando pela factory create ou fromCents. Isso garante que qualquer instância que exista no si


Leia o artigo completo em https://www.vivodecodigo.com.br/backend/ddd-pratica-typescript-prisma-domain-driven-design

Carregando publicação patrocinada...
3
1

Meus 2 cents,

Parabens pelo post !

Conseguir determinar quando usar o DDD eh um desafio - mas confesso que no desenvolvimento usando SDD/TDD/Skills isso acaba sendo bastante interessante: tenho criado specs que descrevem determinados tipos e suas regras de negocio/validacoes para serem usadas independente do tamanho do projeto - esta virando parte do meu harness, com o objetivo especifico de botar o IA/Agente nos eixos e evitar que ele "invente moda".

Tem dado trabalho e as vezes parece over-engineering, mas por outro lado como muita coisa se repete de um projeto para outro, sinto que tem valido a pena - mas ainda esta muito cedo para bater o martelo: pode ser que ao revisitar isso daqui 1 ano veja que foi desperdicio de tempo/energia (ou algo genial, vai saber...).

Obrigado por compartilhar !

Saude e Sucesso !


Este post foi favoritado via extensão TABNEWS FAVORITOS

Tem curiosidade sobre IA ? Da uma olhada no meu LIVRO: IA PARA ENGENHEIROS