Do Zero ao Endpoint: Criando um CRUD no Spring Boot e Entendendo Cada Camada
Criar um CRUD é o primeiro grande passo para dominar o Spring Boot. Mas muito além de copiar e colar código, você sabe o que cada anotação faz e por que dividimos o projeto em camadas? Neste guia prático, vamos construir um CRUD de Produtos do absoluto zero, dissecando a responsabilidade da Entidade, Repository, Service e Controller, e entendendo como o Spring gerencia tudo por baixo dos panos.
Por que dividimos nossa aplicação em camadas?
Quando estamos aprendendo, pode parecer exagero criar vários arquivos diferentes para fazer um simples salvamento no banco de dados. Mas no mundo real, a Separação de Responsabilidades é o que mantém o software manutenível.
Se colocarmos a regra de negócio, a validação, o acesso ao banco e o protocolo HTTP no mesmo arquivo, o código vira uma bagunça impossível de testar ou evoluir. No ecossistema Spring, geralmente, dividimos a aplicação em quatro pilares. Vamos construir e entender cada um deles agora.
1. A Camada de Domínio: A Entidade (@Entity)
A Entidade é a representação da nossa tabela do banco de dados no mundo Java (paradigma orientado a objetos).
package com.alexsousadev.produto.domain;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "tb_produto")
public class Produto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nome;
private BigDecimal preco;
// Construtor padrão exigido pela JPA
public Produto() {}
// Construtor completo, Getters e Setters
}
O que você precisa saber aqui:
@Entity: Diz para a JPA (Jakarta Persistence) que esta classe mapeia uma tabela no banco de dados. O Spring vai ler essa anotação e entender que esse objeto precisa ser gerenciado pelo banco.
@Table(name = "tb_produto"): Opcional, mas recomendada. Define explicitamente o nome da tabela no banco.
@Id e @GeneratedValue: Identificam a chave primária da tabela. O GenerationType.IDENTITY avisa que o banco de dados vai cuidar de auto-incrementar o ID (como o SERIAL no PostgreSQL ou AUTO_INCREMENT no MySQL).
2. A Camada de Acesso aos Dados: O Repository (@Repository)
Esta camada conversa diretamente com o banco de dados. Graças ao Spring Data JPA, nós não precisamos escrever códigos SQL complexos (SELECT * FROM...) para operações básicas.
package com.alexsousadev.produto.repository;
import com.alexsousadev.produto.domain.Produto;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProdutoRepository extends JpaRepository<Produto, Long> {
}
O que você precisa saber aqui:
@Repository: Registra essa interface como um Spring Bean. O Spring lê essa anotação e cria uma implementação automática dessa interface em tempo de execução.
JpaRepository<Produto, Long>: Ao estender essa interface do Spring, ganhamos de graça métodos prontos como .save(), .findAll(), .findById(), e .deleteById(). Passamos a Entidade (Produto) e o tipo do ID (Long) como argumentos genéricos.
3. A Camada de Regras de Negócio: O Service (@Service)
O Service é o "cérebro" da aplicação. O Controller apenas recebe os dados e o Repository apenas salva. Quem decide as regras (ex: "não posso cadastrar um produto com preço negativo" ou "enviar e-mail após o cadastro") é o Service.
package com.alexsousadev.produto.service;
import com.alexsousadev.produto.domain.Produto;
import com.alexsousadev.produto.repository.ProdutoRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProdutoService {
private final ProdutoRepository repository;
// Injeção de Dependência via Construtor
public ProdutoService(ProdutoRepository repository) {
this.repository = repository;
}
public List<Produto> listarTodos() {
return repository.findAll();
}
public Produto salvar(Produto produto) {
// Aqui entrariam as validações de regra de negócio
return repository.save(produto);
}
public Produto buscarPorId(Long id) {
return repository.findById(id)
.orElseThrow(() -> new RuntimeException("Produto não encontrado!"));
}
public void deletar(Long id) {
Produto produto = buscarPorId(id);
repository.delete(produto);
}
}
O que você precisa saber aqui:
@Service: Informa ao Spring que esta classe contém a lógica de negócio do sistema. Ela se torna um Bean gerenciado pelo container do framework.
Injeção via Construtor: Lembra da Inversão de Controle? Nós não damos new ProdutoRepository(). Nós declaramos a dependência no construtor e o Spring injeta a instância correta de forma automática.
4. A Porta de Entrada: O Controller (@RestController)
O Controller é a camada mais externa. Ele é responsável por expor os endpoints da nossa API e receber as requisições HTTP do Front-end(Web ou Mobile)
package com.alexsousadev.produto.controller;
import com.alexsousadev.produto.domain.Produto;
import com.alexsousadev.produto.service.ProdutoService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/produtos")
public class ProdutoController {
private final ProdutoService service;
public ProdutoController(ProdutoService service) {
this.service = service;
}
@GetMapping
public ResponseEntity<List<Produto>> listar() {
return ResponseEntity.ok(service.listarTodos());
}
@GetMapping("/{id}")
public ResponseEntity<Produto> buscar(@PathVariable Long id) {
return ResponseEntity.ok(service.buscarPorId(id));
}
@PostMapping
public ResponseEntity<Produto> criar(@RequestBody Produto produto) {
Produto novoProduto = service.salvar(produto);
return ResponseEntity.status(HttpStatus.CREATED).body(novoProduto);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletar(@PathVariable Long id) {
service.deletar(id);
return ResponseEntity.noContent().build();
}
}
O que você precisa saber aqui:
@RestController: Combina @Controller com @ResponseBody. Diz ao Spring que as respostas dos métodos devem ser serializadas diretamente no formato JSON no corpo da resposta HTTP.
@RequestMapping("/produtos"): Define a rota base deste controller. Toda requisição enviada para http://localhost:8080/produtos cairá aqui.
Verbos HTTP (@GetMapping, @PostMapping, @DeleteMapping): Mapeiam o tipo de operação que o cliente deseja fazer.
@RequestBody: Captura o JSON enviado pelo Front-end no corpo da requisição e o transforma automaticamente em um objeto Java Produto.
@PathVariable: Vincula a variável colocada na URL (ex: /produtos/5) ao parâmetro Long id do método Java.
ResponseEntity: Nos dá controle total sobre os códigos de status HTTP devolvidos (200 OK, 201 Created, 204 No Content). Seguir as convenções REST é fundamental para construir APIs profissionais.
Conclusão: O Ciclo de um Spring Bean
Percebeu como as camadas se encaixam como engrenagens? Quando a aplicação sobe, o Spring escaneia as anotações (@Repository, @Service, @RestController) e cria as instâncias de cada classe na memória do servidor.
Quando uma requisição de cadastro chega no Controller, ele delega o fluxo para o Service, que executa as regras e aciona o Repository, que por fim persiste a Entidade no banco
Desafio:
Preparei um desafio para você, link do repositório abaixo:
2 comentários
Parabéns pela explicação! Muito bom!
Top sua explicação, até quem não tem tanto conhecimento em Spring consegue entender agora.