APIs REST e Bounded Contexts
Camadas: controller, service, repository, DTO
Você aplica separação controller/service/repository com exemplos concretos em Java e Python.
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
- 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"));
}
}
- 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));
}
}
- 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
- 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
- Subir ambiente:
docker compose -f infra/docker-compose.yml up --build -d
- 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.
- 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.