APIs REST e Bounded Contexts

Camadas: controller, service, repository, DTO

Você aplica separação controller/service/repository com exemplos concretos em Java e Python.

Avançado 50 min 30 pontos Leitura 0%

Nesta aula você vai

  • Separar entrada HTTP, regra de negócio e persistência
  • Padronizar validação e resposta de erro
  • Facilitar testes de unidade por responsabilidade

Camadas: controller, service, repository, DTO

Objetivos

  • Separar entrada HTTP, regra de negócio e persistência
  • Padronizar validação e resposta de erro
  • Facilitar testes de unidade por responsabilidade

Pré-requisitos

  • Aula de bounded contexts concluída
  • Serviços básicos em execução
  • Conhecimento de classes/funções em sua linguagem principal

Conceito

Quando o código HTTP conversa direto com banco, o acoplamento explode: validação, regra de negócio e persistência ficam misturadas. O resultado é endpoint difícil de testar e de evoluir. A arquitetura em camadas resolve isso ao separar papéis.

controller recebe requisição e retorna resposta. service contém regra de negócio. repository lida com armazenamento. Essa separação não é burocracia: ela reduz regressão e melhora legibilidade em qualquer stack.

Nesta aula, você aplica esse padrão no customer-service (Spring) e no order-service (FastAPI), e vê como replicar a mesma ideia em Go, Ruby e Node sem copiar framework específico.

Estrutura de arquivos

services/
  customer-service/src/main/java/com/aprendi/customer/
    controller/CustomerController.java
    service/CustomerService.java
    repository/CustomerRepository.java
  order-service/app/
    api/orders.py
    service/order_service.py
    repository/order_repository.py

Passo a passo

  1. Implementar controller no Spring (CustomerController.java)
package com.aprendi.customer.controller;

import com.aprendi.customer.service.CustomerService;
import java.util.Map;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/customers")
public class CustomerController {
  private final CustomerService service;

  public CustomerController(CustomerService service) {
    this.service = service;
  }

  @PostMapping
  public Map<String, String> create(@RequestBody Map<String, String> body) {
    return service.create(body.get("fullName"), body.get("email"));
  }
}
  1. Implementar service e repository no Spring
// CustomerService.java
package com.aprendi.customer.service;

import com.aprendi.customer.repository.CustomerRepository;
import java.util.Map;
import java.util.UUID;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {
  private final CustomerRepository repository;

  public CustomerService(CustomerRepository repository) {
    this.repository = repository;
  }

  public Map<String, String> create(String fullName, String email) {
    String id = UUID.randomUUID().toString();
    repository.save(id, fullName, email);
    return Map.of("id", id, "fullName", fullName, "email", email);
  }
}
// CustomerRepository.java
package com.aprendi.customer.repository;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Repository;

@Repository
public class CustomerRepository {
  private final Map<String, Map<String, String>> db = new ConcurrentHashMap<>();

  public void save(String id, String fullName, String email) {
    db.put(id, Map.of("id", id, "fullName", fullName, "email", email));
  }
}
  1. Aplicar o mesmo padrão no FastAPI (orders.py, order_service.py, order_repository.py)
# api/orders.py
from fastapi import APIRouter
from app.service.order_service import OrderService
from pydantic import BaseModel

router = APIRouter(prefix="/orders", tags=["orders"])
service = OrderService()

class CreateOrderIn(BaseModel):
    customer_id: str
    total_amount: float

@router.post("")
def create_order(payload: CreateOrderIn):
    return service.create(payload.customer_id, payload.total_amount)
# service/order_service.py
import uuid
from app.repository.order_repository import OrderRepository

class OrderService:
    def __init__(self):
        self.repo = OrderRepository()

    def create(self, customer_id: str, total_amount: float):
        order_id = str(uuid.uuid4())
        order = {
            "id": order_id,
            "customer_id": customer_id,
            "total_amount": total_amount,
            "status": "PENDING_PAYMENT",
        }
        self.repo.save(order)
        return order
# repository/order_repository.py
class OrderRepository:
    _db = {}

    def save(self, order: dict):
        self._db[order["id"]] = order
  1. Exemplo equivalente em Node (analytics-service) para reforçar padrão
// service/metricService.js
class MetricService {
  constructor(repository) {
    this.repository = repository;
  }
  register(eventType) {
    return this.repository.increment(eventType);
  }
}
module.exports = { MetricService };

Como testar

  1. Subir ambiente:
docker compose -f infra/docker-compose.yml up --build -d
  1. Criar cliente:
curl -s -X POST http://localhost:8081/customers \
  -H "Content-Type: application/json" \
  -d '{"fullName":"Ana Silva","email":"ana@aprendi.dev"}'

Saída esperada: JSON com id, fullName e email.

  1. Criar pedido:
curl -s -X POST http://localhost:8000/orders \
  -H "Content-Type: application/json" \
  -d '{"customer_id":"c-001","total_amount":129.90}'

Saída esperada: JSON com status: "PENDING_PAYMENT".

Dicas de projeto

  • Valide dados de entrada no controller, não no repository.
  • Evite retorno de entidade interna diretamente para HTTP.
  • Escreva testes focados no service para regra de negócio.
  • Centralize tratamento de erro para manter API previsível.

Erros comuns

  • Controller chamando banco direto.
  • Service com dependência de framework web.
  • Repository com regra de negócio.
  • DTO e validação espalhados sem padrão.

Resumo

A arquitetura em camadas foi aplicada de forma prática em Java e Python, com padrão replicável nas outras stacks. Isso reduz acoplamento e prepara o código para evoluir com eventos nas próximas aulas.