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();
    }
}