Desenvolvimento · Java & Spring Boot

Spring Boot para iniciantes: criando sua primeira API REST em Java

Edson Feitosa de Oliveira Junior
Edson Feitosa de Oliveira Junior Fundador e Engenheiro Líder · PruPru Digital Services

Para criar sua primeira API REST com Spring Boot, você precisa de quatro peças: um projeto gerado no Spring Initializr, uma entidade JPA que representa a tabela, um repository para acessar o banco e um controller que recebe as requisições HTTP. Com Java 17 ou mais recente e Spring Boot 3.x, dá para ter um CRUD funcionando, com validação e tratamento de erros, em uma tarde de estudo.

Neste tutorial vamos montar uma API de cadastro de clientes, camada por camada, explicando o motivo de cada decisão. Se os termos GET, POST, status 201 ou JSON ainda são novos, leia antes o que é uma API REST.

1. Gerando o projeto no Spring Initializr

Acesse start.spring.io e configure:

Clique em Generate, descompacte e abra a pasta no IntelliJ IDEA, VS Code (com o Extension Pack for Java) ou Eclipse. O H2 é um banco em memória, ótimo para aprender: não precisa instalar nada, e os dados somem quando a aplicação para. Depois você troca por PostgreSQL ou MySQL mudando a dependência e as propriedades de conexão.

Rode o projeto com ./mvnw spring-boot:run (no Windows, mvnw.cmd spring-boot:run). Se aparecer "Started ClientesApiApplication", está funcionando na porta 8080.

2. Estrutura do projeto

Organize o código em pacotes por responsabilidade. Para um projeto pequeno, esta divisão é suficiente:

src/main/java/br/com/exemplo/clientesapi/
  ClientesApiApplication.java
  cliente/
    Cliente.java              (entidade JPA)
    ClienteRepository.java    (acesso ao banco)
    ClienteService.java       (regras de negócio)
    ClienteController.java    (endpoints HTTP)
    ClienteRequest.java       (dados de entrada)
    ClienteResponse.java      (dados de saída)
  erro/
    RecursoNaoEncontradoException.java
    TratadorDeErros.java

Por que separar controller e service? O controller cuida só de HTTP (receber, validar formato, devolver status). O service concentra as regras, o que facilita testes e reaproveitamento.

3. A entidade e o repository

package br.com.exemplo.clientesapi.cliente;

import jakarta.persistence.*;

@Entity
@Table(name = "clientes")
public class Cliente {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String nome;

    @Column(nullable = false, unique = true, length = 150)
    private String email;

    protected Cliente() { } // exigido pelo JPA

    public Cliente(String nome, String email) {
        this.nome = nome;
        this.email = email;
    }

    public void atualizar(String nome, String email) {
        this.nome = nome;
        this.email = email;
    }

    public Long getId() { return id; }
    public String getNome() { return nome; }
    public String getEmail() { return email; }
}

Repare no pacote jakarta.persistence: no Spring Boot 3 os antigos imports javax.persistence não funcionam mais. Esse é o erro mais comum de quem copia tutoriais antigos.

package br.com.exemplo.clientesapi.cliente;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ClienteRepository extends JpaRepository<Cliente, Long> {
    boolean existsByEmail(String email);
}

Só com essa interface você ganha findAll, findById, save, deleteById e outros. O método existsByEmail é gerado automaticamente a partir do nome.

4. DTOs com validação

Não exponha a entidade diretamente na API. Records do Java são perfeitos para isso:

package br.com.exemplo.clientesapi.cliente;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record ClienteRequest(
        @NotBlank @Size(max = 100) String nome,
        @NotBlank @Email @Size(max = 150) String email) { }
package br.com.exemplo.clientesapi.cliente;

public record ClienteResponse(Long id, String nome, String email) {
    static ClienteResponse de(Cliente c) {
        return new ClienteResponse(c.getId(), c.getNome(), c.getEmail());
    }
}

5. O service

package br.com.exemplo.clientesapi.cliente;

import br.com.exemplo.clientesapi.erro.RecursoNaoEncontradoException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class ClienteService {

    private final ClienteRepository repository;

    public ClienteService(ClienteRepository repository) {
        this.repository = repository; // injeção pelo construtor
    }

    public List<ClienteResponse> listar() {
        return repository.findAll().stream().map(ClienteResponse::de).toList();
    }

    public ClienteResponse buscar(Long id) {
        return ClienteResponse.de(encontrar(id));
    }

    @Transactional
    public ClienteResponse criar(ClienteRequest req) {
        if (repository.existsByEmail(req.email())) {
            throw new IllegalArgumentException("E-mail já cadastrado");
        }
        return ClienteResponse.de(repository.save(new Cliente(req.nome(), req.email())));
    }

    @Transactional
    public ClienteResponse atualizar(Long id, ClienteRequest req) {
        Cliente c = encontrar(id);
        c.atualizar(req.nome(), req.email()); // salvo ao fim da transação
        return ClienteResponse.de(c);
    }

    @Transactional
    public void remover(Long id) {
        repository.delete(encontrar(id));
    }

    private Cliente encontrar(Long id) {
        return repository.findById(id)
                .orElseThrow(() -> new RecursoNaoEncontradoException("Cliente " + id + " não encontrado"));
    }
}

6. O controller

package br.com.exemplo.clientesapi.cliente;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/clientes")
public class ClienteController {

    private final ClienteService service;

    public ClienteController(ClienteService service) {
        this.service = service;
    }

    @GetMapping
    public List<ClienteResponse> listar() {
        return service.listar();
    }

    @GetMapping("/{id}")
    public ClienteResponse buscar(@PathVariable Long id) {
        return service.buscar(id);
    }

    @PostMapping
    public ResponseEntity<ClienteResponse> criar(@RequestBody @Valid ClienteRequest req) {
        ClienteResponse criado = service.criar(req);
        return ResponseEntity.created(URI.create("/clientes/" + criado.id())).body(criado);
    }

    @PutMapping("/{id}")
    public ClienteResponse atualizar(@PathVariable Long id, @RequestBody @Valid ClienteRequest req) {
        return service.atualizar(id, req);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> remover(@PathVariable Long id) {
        service.remover(id);
        return ResponseEntity.noContent().build();
    }
}

O @Valid faz o Spring checar as anotações do record antes de chamar o método. Se algo falhar, ele lança MethodArgumentNotValidException, que vamos tratar a seguir.

7. Tratamento de erros centralizado

package br.com.exemplo.clientesapi.erro;

public class RecursoNaoEncontradoException extends RuntimeException {
    public RecursoNaoEncontradoException(String msg) { super(msg); }
}
package br.com.exemplo.clientesapi.erro;

import org.springframework.http.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
public class TratadorDeErros {

    @ExceptionHandler(RecursoNaoEncontradoException.class)
    public ProblemDetail naoEncontrado(RecursoNaoEncontradoException e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ProblemDetail conflito(IllegalArgumentException e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail invalido(MethodArgumentNotValidException e) {
        Map<String, String> campos = e.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.toMap(f -> f.getField(),
                        f -> String.valueOf(f.getDefaultMessage()), (a, b) -> a));
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Dados inválidos");
        pd.setProperty("campos", campos);
        return pd;
    }
}

ProblemDetail existe desde o Spring Framework 6 e segue o padrão Problem Details para erros HTTP. Usar IllegalArgumentException para o e-mail duplicado é uma simplificação didática; em projetos maiores, crie exceções específicas para cada regra.

8. Testando

curl -i -X POST http://localhost:8080/clientes -H "Content-Type: application/json" -d "{\"nome\":\"Ana\",\"email\":\"ana@exemplo.com\"}"
curl -i http://localhost:8080/clientes
curl -i http://localhost:8080/clientes/99

Espere 201 com cabeçalho Location no primeiro, 200 com a lista no segundo e 404 com JSON de erro no terceiro. Envie um e-mail inválido e veja o 400 com o campo problemático.

Erros comuns e próximos passos

Depois disso, versione o projeto com Git e GitHub, escreva testes com @WebMvcTest e @DataJpaTest, troque o H2 por PostgreSQL e empacote a aplicação com Docker.

Sites e sistemas sob medida

A PruPru desenvolve APIs e sistemas em Java com Spring Boot para pequenas empresas, da modelagem do banco à publicação.

Ver serviços →

Perguntas frequentes

Preciso saber Java antes de aprender Spring Boot?

Sim, pelo menos o básico: classes, interfaces, coleções, exceções e um pouco de streams. Sem isso, o Spring parece mágica e os erros ficam difíceis de entender.

Qual versão do Java usar com Spring Boot 3?

O Spring Boot 3 exige no mínimo Java 17. Versões LTS como 17 e 21 são as escolhas mais seguras para projetos novos.

Maven ou Gradle?

Os dois funcionam bem. Maven é mais comum em vagas e tutoriais em português, por isso costuma ser mais fácil para começar. Gradle é mais flexível e popular em projetos Android e em algumas empresas.

Como ver os dados do H2 enquanto testo?

Adicione spring.h2.console.enabled=true no application.properties e acesse /h2-console no navegador. Confira a URL do banco no log de inicialização, já que nas versões recentes o nome do banco em memória pode ser gerado automaticamente.

Falar no WhatsApp agora