Criar sua Primeira REST API com Spring Boot
1. Visão geral
Este artigo te guia na criação de uma REST API completa com Spring Boot, usando o exemplo de gerenciamento de Order (pedido). Após ler, você será capaz de construir o projeto, escrever um controller que lida com CRUD, retornar erros de acordo com o padrão HTTP, e escrever testes para a API usando MockMvc.
2. Inicializar o projeto
Crie um projeto via Spring Initializr com as seguintes dependências:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
spring-boot-starter-web fornece Tomcat incorporado e Spring MVC. spring-boot-starter-validation é usado para validar o corpo da requisição (seção 5). A parte spring-boot-starter-test vem com JUnit 5, AssertJ, MockMvc.
Verifique se a aplicação funciona:
@SpringBootApplication
public class OrderApiApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApiApplication.class, args);
}
}
Execute mvn spring-boot:run, você verá o Tomcat iniciando na porta 8080.
3. Definir o modelo
public class Order {
private Long id;
private String customerName;
private BigDecimal amount;
public Order(Long id, String customerName, BigDecimal amount) {
this.id = id;
this.customerName = customerName;
this.amount = amount;
}
// getters, setters
}
Este é um modelo simples, ainda não relacionado com JPA. O objetivo deste artigo é dominar a camada REST primeiro, deixando a persistência para outro artigo.
4. Escrever o Service para armazenar dados temporariamente
@Service
public class OrderService {
private final Map<Long, Order> store = new ConcurrentHashMap<>();
private final AtomicLong idSequence = new AtomicLong(0);
public Order create(Order order) {
long id = idSequence.incrementAndGet();
Order saved = new Order(id, order.getCustomerName(), order.getAmount());
store.put(id, saved);
return saved;
}
public List<Order> findAll() {
return new ArrayList<>(store.values());
}
public Optional<Order> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
public void deleteById(Long id) {
store.remove(id);
}
}
ConcurrentHashMap funciona como um “banco de dados” temporário, permitindo que você execute a demonstração sem precisar configurar um BD. AtomicLong gera IDs sequenciais incrementados, sendo seguro quando múltiplas threads chamam create.
Observação: Esta forma de armazenar dados é apenas para aprendizado, os dados são perdidos quando a app reinicia. Em produção, você precisa de Spring Data JPA ou equivalente.
5. Escrever o REST Controller
5.1. Endpoints para criar e listar
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<Order> create(@Valid @RequestBody OrderRequest request) {
Order order = new Order(null, request.customerName(), request.amount());
Order saved = orderService.create(order);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@GetMapping
public List<Order> findAll() {
return orderService.findAll();
}
}
@RestController combina @Controller e @ResponseBody, retornando JSON em vez de uma view. @RequestMapping("/api/orders") anexa um prefixo comum para todos os endpoints nesta classe. @PostMapping recebe o body via @RequestBody, Spring converte automaticamente JSON para OrderRequest graças ao Jackson incluído em spring-boot-starter-web.
OrderRequest é um record usado para validar entrada, separado de Order:
public record OrderRequest(
@NotBlank String customerName,
@NotNull @Positive BigDecimal amount
) {}
5.2. Endpoints para obter por ID e deletar
@GetMapping("/{id}")
public ResponseEntity<Order> findById(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
orderService.deleteById(id);
return ResponseEntity.noContent().build();
}
@PathVariable captura o valor {id} da URL. Quando o pedido não é encontrado, a API retorna 404 Not Found em vez de null — este é um ponto facilmente negligenciado ao criar sua primeira REST API.
Teste com curl:
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"customerName":"Nguyen Van A","amount":150000}'
HTTP/1.1 201 Created
Content-Type: application/json
{"id":1,"customerName":"Nguyen Van A","amount":150000}
6. Tratamento centralizado de erros
Se não tratar, erros de validação retornarão uma response padrão difícil de ler. Use @RestControllerAdvice para padronizar:
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
}
Esta classe captura qualquer MethodArgumentNotValidException lançada por @Valid, agrupa erros por campo e retorna 400 Bad Request com um body legível:
{
"amount": "must be greater than 0",
"customerName": "must not be blank"
}
Observação: Não deixe exceptions padrão do Spring se exporem em produção — pode expor stack trace e afetar a segurança.
7. Escrever testes
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void shouldReturn201WhenCreateOrderSuccessfully() throws Exception {
Order saved = new Order(1L, "Tran Thi B", new BigDecimal("200000"));
given(orderService.create(any(Order.class))).willReturn(saved);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"customerName":"Tran Thi B","amount":200000}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.customerName").value("Tran Thi B"));
}
@Test
void shouldReturn404WhenOrderNotFound() throws Exception {
given(orderService.findById(99L)).willReturn(Optional.empty());
mockMvc.perform(get("/api/orders/99"))
.andExpect(status().isNotFound());
}
}
@WebMvcTest apenas carrega a camada web (controller, filter, validation), não inicializa o contexto completo, então os testes executam rapidamente. @MockBean substitui o OrderService real por um mock, ajudando a testar o controller de forma independente. Esses dois casos de teste verificam tanto o caminho de sucesso quanto o de erro — um hábito que você deve ter para todo endpoint.
Verificar ainda mais com AssertJ na camada de serviço:
@Test
void shouldGenerateIncrementalIdWhenCreatingMultipleOrders() {
OrderService service = new OrderService();
Order first = service.create(new Order(null, "A", BigDecimal.TEN));
Order second = service.create(new Order(null, "B", BigDecimal.ONE));
assertThat(first.getId()).isEqualTo(1L);
assertThat(second.getId()).isEqualTo(2L);
}
8. Erros comuns
- Esquecer de
@RequestBody: Spring não vinculará JSON ao objeto, o campo sempre ficanullsem aviso claro de erro. - Retornar
nullem vez deResponseEntity.notFound(): cliente recebe200 OKcom bodynull, fácil de causar bug no frontend. - Não validar entrada: API recebe amount negativo, nome vazio… causando erro de lógica em camadas inferiores que é difícil de rastrear.
- Usar
@Controllerem vez de@RestController: deve adicionar@ResponseBodymanualmente em cada método, fácil esquecer.
Observação: Se sua API atende vários clientes (mobile, web, parceiros), considere versionamento desde o início (por exemplo
/api/v1/orders) para evitar quebra de compatibilidade no futuro.
9. Conclusão
Você construiu uma REST API básica com Spring Boot: desde modelo, serviço, controller, tratamento centralizado de erros até testes com MockMvc. Próximo passo, você deve substituir ConcurrentHashMap por Spring Data JPA para que os dados persistam realmente, e adicione @RestControllerAdvice para tratar mais exceções como EntityNotFoundException.