DEV Community

Thiago Matos
Thiago Matos

Posted on

🚀 Criar Endpoint POST /invoices

Para: Desenvolvedor Júnior Iniciante

Objetivo: Criar um endpoint REST para cadastrar faturas

Tempo estimado: 3-4 horas

Nível de dificuldade: ⭐⭐⭐ Intermediário


📋 Pré-requisitos

Antes de começar, certifique-se de que você completou:

  • ✅ Projeto invoice-service criado e funcionando
  • ✅ IntelliJ IDEA configurado corretamente
  • ✅ Aplicação roda em http://localhost:8080
  • ✅ Estrutura de pacotes criada (controller, service, model, repository)

Se algo estiver pendente, volte ao guia Como criar um novo projeto Spring Boot! 😊


🎯 O que vamos construir?

Hoje você vai criar sua primeira API REST funcional! Especificamente:

  1. Modelo de dados Invoice - A estrutura que representa uma fatura
  2. Endpoint POST /invoices - Para criar novas faturas
  3. Validações - Garantir que os dados estão corretos
  4. Testes - Verificar se tudo funciona usando Postman ou cURL

Resultado final:

# Enviar requisição
POST http://localhost:8080/invoices
{
  "customerId": "CUST001",
  "amount": 150.50,
  "status": "PENDING"
}

# Receber resposta 201 Created
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "customerId": "CUST001",
  "amount": 150.50,
  "status": "PENDING",
  "createdAt": "2025-12-29T18:00:00"
}
Enter fullscreen mode Exit fullscreen mode

Legal, né? Vamos lá! 🚀


📦 Parte 1: Criar o Modelo de Dados (Invoice)

O que é um modelo de dados?

É uma classe que representa um objeto do mundo real. No nosso caso, uma fatura (invoice).

Analogia: Pense em um formulário de papel. O modelo é como o template desse formulário, definindo quais campos existem (nome, CPF, valor, etc.).

Passo a Passo

1.1 Criar a Data Class Invoice

  1. No IntelliJ, navegue até: src/main/kotlin/com/invoice/model/
  2. Clique com botão direito na pasta modelNew → Kotlin Class/File
  3. Digite: Invoice
  4. Selecione: Class

1.2 Implementar a Data Class

Abra o arquivo Invoice.kt e adicione o seguinte código:

package com.invoice.model

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Positive
import java.math.BigDecimal
import java.time.LocalDateTime
import java.util.UUID

data class Invoice(
    val id: String = UUID.randomUUID().toString(),

    @field:NotBlank(message = "Customer ID é obrigatório")
    val customerId: String,

    @field:NotNull(message = "Valor é obrigatório")
    @field:Positive(message = "Valor deve ser positivo")
    val amount: BigDecimal,

    @field:NotBlank(message = "Status é obrigatório")
    val status: String,

    val createdAt: LocalDateTime = LocalDateTime.now()
)
Enter fullscreen mode Exit fullscreen mode

1.3 Entendendo o Código Linha por Linha

Vamos explicar cada parte:

1. Imports:

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Positive
Enter fullscreen mode Exit fullscreen mode

Esses imports trazem as anotações de validação. São como "regras" que garantem dados válidos.

2. Data Class:

data class Invoice(...)
Enter fullscreen mode Exit fullscreen mode
  • data class - Tipo especial do Kotlin que gera automaticamente:
    • Método equals() - compara objetos
    • Método hashCode() - útil para coleções
    • Método toString() - mostra os valores
    • Método copy() - cria cópias com alterações

Analogia: É como um atalho que escreve muito código por você!

3. Propriedades:

val id: String = UUID.randomUUID().toString()
Enter fullscreen mode Exit fullscreen mode
  • val - Propriedade imutável (não pode ser alterada depois)
  • id: String - Nome e tipo da propriedade
  • UUID.randomUUID().toString() - Gera um ID único automaticamente
    • Exemplo: "550e8400-e29b-41d4-a716-446655440000"
@field:NotBlank(message = "Customer ID é obrigatório")
val customerId: String
Enter fullscreen mode Exit fullscreen mode
  • @field:NotBlank - Validação: não pode ser vazio ou só espaços
  • message = "..." - Mensagem de erro customizada
  • customerId: String - ID do cliente
@field:NotNull(message = "Valor é obrigatório")
@field:Positive(message = "Valor deve ser positivo")
val amount: BigDecimal
Enter fullscreen mode Exit fullscreen mode
  • BigDecimal - Tipo para valores monetários (mais preciso que Double)
  • @NotNull - Não pode ser nulo
  • @Positive - Deve ser maior que zero

Por que BigDecimal? Números decimais como Double têm problemas de arredondamento. Para dinheiro, use sempre BigDecimal!

val createdAt: LocalDateTime = LocalDateTime.now()
Enter fullscreen mode Exit fullscreen mode
  • LocalDateTime - Data e hora sem timezone
  • LocalDateTime.now() - Pega data/hora atual automaticamente

1.4 Adicionar Dependência de Validação

Para as validações funcionarem, precisamos adicionar a dependência no build.gradle.kts.

  1. Abra o arquivo build.gradle.kts
  2. Localize a seção dependencies
  3. Adicione esta linha:
dependencies {
    // ... dependências existentes ...

    // Validação
    implementation("org.springframework.boot:spring-boot-starter-validation")
}
Enter fullscreen mode Exit fullscreen mode
  1. Clique no ícone 🐘 (elefante do Gradle) que aparece no canto superior direito
  2. Selecione Reload Gradle Project

⏱️ Aguarde alguns segundos enquanto o Gradle baixa a dependência.

1.5 Testar a Data Class

Vamos verificar se a classe compila corretamente:

  1. Clique com botão direito no arquivo Invoice.kt
  2. Selecione Build Module 'invoice-service.main'

Se não aparecer erro, perfeito! Sua data class está pronta.


🎮 Parte 2: Criar o Controller

O que é um Controller?

É a classe que recebe requisições HTTP e as processa. É a "porta de entrada" da sua API.

Analogia: É como o atendente de um restaurante que anota seu pedido e leva para a cozinha.

Passo a Passo

2.1 Criar a Classe InvoiceController

  1. Navegue até: src/main/kotlin/com/invoice/controller/
  2. Clique com botão direito → New → Kotlin Class/File
  3. Digite: InvoiceController
  4. Selecione: Class

2.2 Implementar o Controller Básico

Adicione o seguinte código:

package com.invoice.controller

import com.invoice.model.Invoice
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/invoices")
class InvoiceController {

    // Lista temporária para armazenar faturas (in-memory)
    private val invoices = mutableListOf<Invoice>()

    @PostMapping
    fun createInvoice(@Valid @RequestBody invoice: Invoice): ResponseEntity<Invoice> {
        // Adiciona a fatura na lista
        invoices.add(invoice)

        // Retorna 201 Created com a fatura criada
        return ResponseEntity.status(HttpStatus.CREATED).body(invoice)
    }
}
Enter fullscreen mode Exit fullscreen mode

2.3 Entendendo o Código Linha por Linha

1. Anotações da Classe:

@RestController
@RequestMapping("/invoices")
class InvoiceController
Enter fullscreen mode Exit fullscreen mode
  • @RestController - Diz ao Spring: "Esta classe é um controller REST"

    • Automaticamente converte objetos para JSON
    • Processa requisições HTTP
  • @RequestMapping("/invoices") - Define o caminho base

    • Todas as rotas deste controller começam com /invoices

2. Armazenamento Temporário:

private val invoices = mutableListOf<Invoice>()
Enter fullscreen mode Exit fullscreen mode
  • mutableListOf<Invoice>() - Lista mutável (pode adicionar/remover)
  • Por enquanto, armazena só na memória (quando reiniciar a app, perde tudo)
  • Na quarta-feira vamos adicionar banco de dados real!

3. Método createInvoice:

@PostMapping
fun createInvoice(@Valid @RequestBody invoice: Invoice): ResponseEntity<Invoice>
Enter fullscreen mode Exit fullscreen mode

Quebra completa:

  • @PostMapping - Esta função responde a requisições HTTP POST

    • Caminho completo: POST /invoices (lembra do @RequestMapping?)
  • fun createInvoice - Nome do método (pode ser qualquer nome)

  • @Valid - Valida o objeto usando as anotações que definimos (@notblank, etc.)

    • Se validação falhar, retorna 400 Bad Request automaticamente
  • @RequestBody - O Spring pega o JSON da requisição e converte para objeto Invoice

  • invoice: Invoice - Parâmetro que recebe os dados

  • : ResponseEntity<Invoice> - Tipo de retorno

    • ResponseEntity permite controlar status HTTP (201, 200, 404, etc.)
    • <Invoice> - O corpo da resposta será um objeto Invoice

4. Lógica do Método:

invoices.add(invoice)
Enter fullscreen mode Exit fullscreen mode
  • Adiciona a fatura recebida na lista
return ResponseEntity.status(HttpStatus.CREATED).body(invoice)
Enter fullscreen mode Exit fullscreen mode
  • ResponseEntity.status(HttpStatus.CREATED) - Define status 201 (Created)
  • .body(invoice) - Coloca a fatura no corpo da resposta
  • Spring automaticamente converte para JSON

Por que 201 e não 200?

  • 200 OK: Requisição bem-sucedida (usado em GET, PUT)
  • 201 Created: Recurso criado com sucesso (usado em POST)

É uma boa prática usar o status HTTP correto!


🧪 Parte 3: Testar o Endpoint

Opção A: Testar com cURL (Terminal)

O cURL é uma ferramenta de linha de comando para fazer requisições HTTP.

3.1 Iniciar a Aplicação

  1. No IntelliJ, abra InvoiceServiceApplication.kt
  2. Clique no ▶️ verde ao lado de fun main
  3. Aguarde aparecer: Started InvoiceServiceApplication

3.2 Fazer a Requisição

Abra um novo terminal (fora do IntelliJ) e execute:

curl -X POST http://localhost:8080/invoices \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "CUST001",
    "amount": 150.50,
    "status": "PENDING"
  }'
Enter fullscreen mode Exit fullscreen mode

Explicação do comando:

  • curl - Ferramenta para fazer requisições HTTP
  • -X POST - Método HTTP POST
  • http://localhost:8080/invoices - URL do endpoint
  • -H "Content-Type: application/json" - Header dizendo que enviamos JSON
  • -d '{...}' - Corpo da requisição (dados em JSON)

Resposta esperada:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "customerId": "CUST001",
  "amount": 150.50,
  "status": "PENDING",
  "createdAt": "2025-12-29T18:00:00"
}
Enter fullscreen mode Exit fullscreen mode

Se você viu isso, PARABÉNS! Seu endpoint está funcionando! 🎉

Opção B: Testar com Postman (Recomendado)

Postman é uma ferramenta visual mais amigável para testar APIs.

3.3 Instalar Postman

  1. Acesse: https://www.postman.com/downloads/
  2. Baixe e instale
  3. Crie uma conta gratuita (ou pule)

3.4 Criar a Requisição

  1. Abra o Postman
  2. Clique em New → HTTP Request
  3. Configure:
Campo Valor
Método POST
URL http://localhost:8080/invoices
  1. Vá na aba Body
  2. Selecione raw
  3. No dropdown ao lado, selecione JSON
  4. Cole este JSON:
{
  "customerId": "CUST001",
  "amount": 150.50,
  "status": "PENDING"
}
Enter fullscreen mode Exit fullscreen mode
  1. Clique em Send

Resultado esperado:

  • Status: 201 Created (canto superior direito)
  • Body: JSON com a fatura criada, incluindo id e createdAt

3.5 Testar Validações

Vamos ver se as validações estão funcionando:

Teste 1: Enviar valor negativo

{
  "customerId": "CUST001",
  "amount": -50.00,
  "status": "PENDING"
}
Enter fullscreen mode Exit fullscreen mode

Resultado esperado: 400 Bad Request com mensagem "Valor deve ser positivo"

Teste 2: Enviar customerId vazio

{
  "customerId": "",
  "amount": 100.00,
  "status": "PENDING"
}
Enter fullscreen mode Exit fullscreen mode

Resultado esperado: 400 Bad Request com mensagem "Customer ID é obrigatório"

Teste 3: Não enviar amount

{
  "customerId": "CUST001",
  "status": "PENDING"
}
Enter fullscreen mode Exit fullscreen mode

Resultado esperado: 400 Bad Request com mensagem "Valor é obrigatório"

✅ Se todos os testes passaram, suas validações estão funcionando!


📊 Parte 4: Adicionar Logs para Debug

Logs ajudam a entender o que está acontecendo na aplicação.

4.1 Adicionar Logger no Controller

Atualize seu InvoiceController.kt:

package com.invoice.controller

import com.invoice.model.Invoice
import jakarta.validation.Valid
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/invoices")
class InvoiceController {

    private val logger = LoggerFactory.getLogger(InvoiceController::class.java)
    private val invoices = mutableListOf<Invoice>()

    @PostMapping
    fun createInvoice(@Valid @RequestBody invoice: Invoice): ResponseEntity<Invoice> {
        logger.info("Recebida requisição para criar fatura: {}", invoice)

        invoices.add(invoice)
        logger.info("Fatura criada com sucesso. ID: {}", invoice.id)

        return ResponseEntity.status(HttpStatus.CREATED).body(invoice)
    }
}
Enter fullscreen mode Exit fullscreen mode

O que mudou:

  • private val logger = ... - Cria um objeto logger
  • logger.info("...") - Escreve mensagens no console

4.2 Testar os Logs

  1. Reinicie a aplicação
  2. Faça uma requisição POST
  3. Observe o console do IntelliJ

Você verá algo como:

INFO  c.i.controller.InvoiceController - Recebida requisição para criar fatura: Invoice(id=550e8400..., customerId=CUST001, amount=150.50, status=PENDING, createdAt=2025-12-29T18:00:00)
INFO  c.i.controller.InvoiceController - Fatura criada com sucesso. ID: 550e8400-e29b-41d4-a716-446655440000
Enter fullscreen mode Exit fullscreen mode

🎨 Parte 5: Melhorias Opcionais (Bônus)

5.1 Criar um Enum para Status

Ao invés de aceitar qualquer texto no status, vamos limitar aos valores válidos.

Crie o arquivo: src/main/kotlin/com/invoice/model/InvoiceStatus.kt

package com.invoice.model

enum class InvoiceStatus {
    PENDING,
    PAID,
    CANCELLED,
    OVERDUE
}
Enter fullscreen mode Exit fullscreen mode

Atualize a classe Invoice:

data class Invoice(
    val id: String = UUID.randomUUID().toString(),

    @field:NotBlank(message = "Customer ID é obrigatório")
    val customerId: String,

    @field:NotNull(message = "Valor é obrigatório")
    @field:Positive(message = "Valor deve ser positivo")
    val amount: BigDecimal,

    @field:NotNull(message = "Status é obrigatório")
    val status: InvoiceStatus,  // Mudou aqui!

    val createdAt: LocalDateTime = LocalDateTime.now()
)
Enter fullscreen mode Exit fullscreen mode

Agora o JSON deve ser:

{
  "customerId": "CUST001",
  "amount": 150.50,
  "status": "PENDING"
}
Enter fullscreen mode Exit fullscreen mode

Se enviar status inválido (ex: "INVALID"), retorna erro automaticamente!

5.2 Adicionar Endpoint GET para Listar Faturas

Vamos adicionar um endpoint para ver todas as faturas criadas:

@GetMapping
fun getAllInvoices(): ResponseEntity<List<Invoice>> {
    logger.info("Recebida requisição para listar todas as faturas")
    return ResponseEntity.ok(invoices)
}
Enter fullscreen mode Exit fullscreen mode

Testar:

curl http://localhost:8080/invoices
Enter fullscreen mode Exit fullscreen mode

Ou no Postman: GET http://localhost:8080/invoices


✅ Critérios de Aceite - Checklist Final

Marque cada item que você completou:

Criar Modelo Invoice

  • [ ] Arquivo Invoice.kt criado em model/
  • [ ] Data class com 5 campos: id, customerId, amount, status, createdAt
  • [ ] Validações adicionadas: @notblank, @NotNull, @positive
  • [ ] Dependência spring-boot-starter-validation adicionada no build.gradle.kts
  • [ ] Classe compila sem erros

Criar InvoiceController

  • [ ] Arquivo InvoiceController.kt criado em controller/
  • [ ] Classe anotada com @RestController
  • [ ] @RequestMapping("/invoices") configurado
  • [ ] Lista in-memory criada (mutableListOf)
  • [ ] Método createInvoice implementado
  • [ ] Método anotado com @PostMapping
  • [ ] Parâmetro com @valid e @RequestBody
  • [ ] Retorna ResponseEntity com status 201

Testar Endpoint

  • [ ] Aplicação inicia sem erros
  • [ ] Requisição POST retorna 201 Created
  • [ ] Resposta contém id gerado automaticamente
  • [ ] Resposta contém createdAt gerado automaticamente
  • [ ] Validações funcionam (testes com dados inválidos retornam 400)
  • [ ] Logs aparecem no console

Bônus (Opcional)

  • [ ] Enum InvoiceStatus criado
  • [ ] Status aceita apenas valores do enum
  • [ ] Endpoint GET /invoices implementado
  • [ ] GET retorna lista de faturas criadas

🐛 Problemas Comuns e Soluções

Erro: "Unresolved reference: NotBlank"

Causa: Dependência de validação não foi adicionada

Solução:

  1. Verifique se adicionou spring-boot-starter-validation no build.gradle.kts
  2. Clique no ícone Gradle para recarregar as dependências
  3. Aguarde o download completar

Erro: 404 Not Found ao fazer POST

Causa: Aplicação não está rodando ou URL incorreta

Solução:

  1. Verifique se a aplicação está rodando (console deve mostrar "Started InvoiceServiceApplication")
  2. Confirme a URL: http://localhost:8080/invoices (sem /api)
  3. Verifique o método: deve ser POST, não GET

Erro: 400 Bad Request sem mensagem clara

Causa: JSON malformado

Solução:

  1. Use um validador JSON online: jsonlint.com
  2. Verifique vírgulas, aspas e chaves
  3. Exemplo correto:
{
  "customerId": "CUST001",
  "amount": 150.50,
  "status": "PENDING"
}
Enter fullscreen mode Exit fullscreen mode

Erro: "HttpMessageNotReadableException"

Causa: Tipo de dado incorreto no JSON

Solução:

  • amount deve ser número: 150.50 (sem aspas)
  • customerId e status devem ser texto: "CUST001" (com aspas)

Validações não funcionam (aceita dados inválidos)

Causa: Esqueceu @Valid no parâmetro

Solução:

Verifique se o método tem:

fun createInvoice(@Valid @RequestBody invoice: Invoice)
                  ^^^^^^
Enter fullscreen mode Exit fullscreen mode

Resposta retorna 200 ao invés de 201

Causa: Esqueceu de definir o status HTTP

Solução:

Retorne com:

return ResponseEntity.status(HttpStatus.CREATED).body(invoice)
Enter fullscreen mode Exit fullscreen mode

Não use apenas:

return ResponseEntity.ok(invoice) // Retorna 200
Enter fullscreen mode Exit fullscreen mode

🎓 Conceitos Importantes para Entender

O que é REST?

REST (Representational State Transfer) é um estilo de arquitetura para APIs web.

Princípios básicos:

  • Recursos são identificados por URLs (/invoices)
  • Usam métodos HTTP padrão (GET, POST, PUT, DELETE)
  • São stateless (cada requisição é independente)
  • Respostas geralmente em JSON

Analogia: É como organizar uma biblioteca:

  • Cada livro tem uma identificação única (URL)
  • Ações padronizadas: pegar (GET), adicionar (POST), atualizar (PUT), remover (DELETE)

O que é JSON?

JSON (JavaScript Object Notation) é um formato de texto para trocar dados.

Exemplo:

{
  "nome": "João",
  "idade": 25,
  "ativo": true
}
Enter fullscreen mode Exit fullscreen mode

Por que usamos JSON?

  • Fácil de ler para humanos
  • Fácil de processar para computadores
  • Padrão da indústria para APIs

HTTP Status Codes Importantes

Código Nome Quando usar
200 OK Requisição bem-sucedida (GET, PUT)
201 Created Recurso criado com sucesso (POST)
400 Bad Request Dados inválidos enviados
404 Not Found Recurso não encontrado
500 Internal Server Error Erro no servidor

O que é Serialização/Desserialização?

  • Serialização: Converter objeto Java/Kotlin → JSON
  • Desserialização: Converter JSON → objeto Java/Kotlin

O Spring faz isso automaticamente usando a biblioteca Jackson!

// Objeto Kotlin
Invoice(id="123", customerId="CUST001", ...)

      Serialização (automática)

// JSON
{"id":"123","customerId":"CUST001",...}
Enter fullscreen mode Exit fullscreen mode

O que é Validation?

Garantir que os dados recebidos estão corretos antes de processar.

Sem validação:

{
  "customerId": "",
  "amount": -100,
  "status": "XPTO"
}
Enter fullscreen mode Exit fullscreen mode

Isso causaria problemas! Com validação, rejeitamos automaticamente.


🎯 Próximos Passos

Depois de completar esta atividade, você deve:

  1. Commitar no Git - Salve seu progresso!
git add .
git commit -m "feat: adiciona endpoint POST /invoices e modelo Invoice"
Enter fullscreen mode Exit fullscreen mode
  1. Testar mais cenários - Experimente diferentes dados
  2. Mostrar para o alguém - Demonstre funcionando!

💡 Dicas

Teste sempre que mudar algo!

Não acumule mudanças. Faça pequenas alterações e teste cada uma.

Leia as mensagens de erro com atenção

Elas geralmente dizem exatamente qual é o problema. Procure a última linha da stack trace.

Use logs para debug

Quando algo não funciona como esperado, adicione logger.info() para ver o que está acontecendo.

Não decore, entenda!

É melhor entender o conceito do que decorar código. Se entender, saberá adaptar para outros cenários.

Postman é seu amigo

Salve suas requisições no Postman. Você vai reutilizá-las muito!


🎉 Parabéns

Se você chegou até aqui e seu endpoint está funcionando, você acabou de:

✅ Criar seu primeiro modelo de dados com validações

✅ Implementar seu primeiro endpoint REST POST

✅ Entender serialização JSON

✅ Testar APIs usando Postman/cURL

✅ Adicionar logs para debug

Você está oficialmente criando APIs profissionais! 🚀

Isso é incrível para alguém que há pouco tempo era estagiário! Você já está produzindo código que roda em produção em muitas empresas.

Continue assim! Cada linha de código é um passo a mais na sua jornada. 💪

Top comments (0)