Capítulo 03: Arquitetura de Controllers REST (@RestController) e DTOs
Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Requisição HTTP"] --> B["@RestController"]
B --> C["@RequestBody: ProdutoRequestDTO (Record)"]
C --> D["Service Layer"]
D --> E["ResponseEntity<ProdutoResponseDTO>"]
E --> F["JSON + Status Code Semântico"]
F --> G["Pageable / Page<T> para Listagens"]
G --> H["HATEOAS: _links.self"]
subgraph ARQ["Arquitetura do Capítulo"]
I["@Controller (View) vs @RestController (JSON direto)"]
J["DTOs como Records imutáveis"]
K["Paginação e Hipermídia"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos: Controllers REST, DTOs e Contratos de API
@Controller vs @RestController. No Spring MVC clássico, @Controller retorna o nome de uma view (ex.: um template Thymeleaf) que o ViewResolver renderiza em HTML no servidor. @RestController é uma meta-anotação que combina @Controller + @ResponseBody: o retorno do método é serializado diretamente (por padrão em JSON, via Jackson) e escrito no corpo da resposta HTTP — é o padrão para APIs REST desacopladas de frontend.
DTOs como Java Records. Expor entidades JPA diretamente em um Controller é um antipadrão grave: acopla o contrato público da API ao esquema interno do banco, vaza detalhes de implementação (relacionamentos lazy, IDs internos) e impede evoluir o schema sem quebrar clientes. O DTO (Data Transfer Object) resolve isso, e desde o Java 16/21 o Record é a forma idiomática de declará-lo: imutável, com equals()/hashCode()/toString() gerados automaticamente, e compatível nativamente com Jackson para (de)serialização:
public record ProdutoRequestDTO(
@NotBlank String nome,
@Positive BigDecimal preco
) {}
public record ProdutoResponseDTO(Long id, String nome, BigDecimal preco) {}
Separar DTO de entrada (RequestDTO) do de saída (ResponseDTO) evita expor campos que o cliente não deveria enviar (como id ou criadoEm) e permite formatos de resposta enriquecidos (campos calculados, links).
Respostas HTTP semânticas com ResponseEntity. Retornar apenas o corpo (ProdutoResponseDTO) força o Spring a usar sempre 200 OK. ResponseEntity<T> dá controle total sobre status code, headers e corpo — essencial para 201 Created (com header Location apontando para o recurso criado), 204 No Content (DELETE bem-sucedido) ou 404 Not Found.
Paginação com Pageable. Listar todos os registros de uma tabela sem paginação é um risco de performance e memória em produção. O Spring Data resolve isso de forma transparente: basta o Controller receber Pageable como parâmetro (o Spring extrai page, size e sort da query string automaticamente) e devolver Page<T>, que já serializa metadados (totalElements, totalPages, number) junto ao conteúdo.
HATEOAS. Em APIs REST maduras (nível 3 de Richardson), a resposta não deve apenas conter dados, mas também links de navegação (_links.self, _links.next) que descrevem as ações disponíveis a partir daquele recurso — o Spring HATEOAS (EntityModel<T>, WebMvcLinkBuilder) automatiza a construção desses links sem hardcodar URLs.
💻 2. Código de Demonstração Corporativo
package com.empresa.api.produto.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import java.math.BigDecimal;
public record ProdutoRequestDTO(
@NotBlank String nome,
@Positive BigDecimal preco
) {}
package com.empresa.api.produto.dto;
import java.math.BigDecimal;
public record ProdutoResponseDTO(Long id, String nome, BigDecimal preco) {}
package com.empresa.api.produto;
import com.empresa.api.produto.dto.ProdutoRequestDTO;
import com.empresa.api.produto.dto.ProdutoResponseDTO;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
@RestController
@RequestMapping("/api/v1/produtos")
public class ProdutoController {
private final ProdutoService service;
public ProdutoController(ProdutoService service) {
this.service = service;
}
@GetMapping
public ResponseEntity<Page<ProdutoResponseDTO>> listar(Pageable pageable) {
return ResponseEntity.ok(service.listar(pageable));
}
@GetMapping("/{id}")
public ResponseEntity<ProdutoResponseDTO> buscarPorId(@PathVariable Long id) {
return ResponseEntity.ok(service.buscarPorId(id));
}
@PostMapping
public ResponseEntity<ProdutoResponseDTO> criar(@Valid @RequestBody ProdutoRequestDTO dto) {
ProdutoResponseDTO criado = service.criar(dto);
URI location = URI.create("/api/v1/produtos/" + criado.id());
return ResponseEntity.created(location).body(criado);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletar(@PathVariable Long id) {
service.deletar(id);
return ResponseEntity.noContent().build();
}
}