REST APIs sao a espinha dorsal da comunicacao entre sistemas modernos. Projetar APIs bem estruturadas, consistentes e documentadas e uma habilidade essencial para qualquer desenvolvedor backend. Kotlin, com sua expressividade e seguranca de tipos, e uma excelente escolha para construir APIs que sao ao mesmo tempo robustas e faceis de manter. Neste guia, vamos alem do basico de rotas e controllers, abordando design de recursos, validacao, tratamento de erros, paginacao, versionamento e documentacao com OpenAPI.
Principios de Design REST
Uma API RESTful segue convencoes que tornam o uso intuitivo para consumidores. Os principios fundamentais incluem: recursos identificados por URLs, verbos HTTP para operacoes, respostas com codigos de status apropriados e representacoes em JSON.
// Bom design de URLs (recursos no plural, hierarquia clara)
// GET /api/v1/clientes -> Listar clientes
// GET /api/v1/clientes/{id} -> Buscar cliente
// POST /api/v1/clientes -> Criar cliente
// PUT /api/v1/clientes/{id} -> Atualizar cliente
// DELETE /api/v1/clientes/{id} -> Remover cliente
// GET /api/v1/clientes/{id}/pedidos -> Listar pedidos do cliente
Estrutura de Projeto
Uma API bem organizada separa responsabilidades em camadas:
// Estrutura de pacotes
// com.exemplo.api/
// controller/ -> Endpoints REST
// service/ -> Logica de negocio
// repository/ -> Acesso a dados
// model/
// entity/ -> Entidades do banco
// request/ -> DTOs de entrada
// response/ -> DTOs de saida
// config/ -> Configuracoes
// exception/ -> Excecoes e handlers
// validation/ -> Validadores customizados
DTOs Robustos com Validacao
Data classes do Kotlin combinadas com Bean Validation criam DTOs expressivos:
data class CriarClienteRequest(
@field:NotBlank(message = "Nome e obrigatorio")
@field:Size(min = 2, max = 100, message = "Nome deve ter entre 2 e 100 caracteres")
val nome: String,
@field:NotBlank(message = "Email e obrigatorio")
@field:Email(message = "Email invalido")
val email: String,
@field:NotBlank(message = "CPF e obrigatorio")
@field:Pattern(
regexp = "\\d{11}",
message = "CPF deve conter 11 digitos"
)
val cpf: String,
@field:Past(message = "Data de nascimento deve ser no passado")
val dataNascimento: LocalDate? = null,
val endereco: EnderecoRequest? = null
)
data class EnderecoRequest(
@field:NotBlank(message = "CEP e obrigatorio")
@field:Pattern(regexp = "\\d{8}", message = "CEP deve conter 8 digitos")
val cep: String,
@field:NotBlank(message = "Logradouro e obrigatorio")
val logradouro: String,
val numero: String? = null,
val complemento: String? = null,
@field:NotBlank(message = "Cidade e obrigatoria")
val cidade: String,
@field:NotBlank(message = "UF e obrigatoria")
@field:Size(min = 2, max = 2, message = "UF deve ter 2 caracteres")
val uf: String
)
data class ClienteResponse(
val id: Long,
val nome: String,
val email: String,
val cpf: String,
val dataNascimento: LocalDate?,
val endereco: EnderecoResponse?,
val criadoEm: LocalDateTime,
val atualizadoEm: LocalDateTime?
)
// Response paginado generico
data class PaginaResponse<T>(
val conteudo: List<T>,
val pagina: Int,
val tamanho: Int,
val totalElementos: Long,
val totalPaginas: Int,
val primeira: Boolean,
val ultima: Boolean
)
Controller Completo
@RestController
@RequestMapping("/api/v1/clientes")
class ClienteController(
private val clienteService: ClienteService
) {
@GetMapping
fun listar(
@RequestParam(defaultValue = "0") pagina: Int,
@RequestParam(defaultValue = "20") tamanho: Int,
@RequestParam(defaultValue = "nome") ordenarPor: String,
@RequestParam(defaultValue = "ASC") direcao: String,
@RequestParam(required = false) busca: String?
): ResponseEntity<PaginaResponse<ClienteResponse>> {
val resultado = clienteService.listar(
pagina = pagina,
tamanho = tamanho.coerceIn(1, 100),
ordenarPor = ordenarPor,
direcao = Sort.Direction.valueOf(direcao),
busca = busca
)
return ResponseEntity.ok(resultado)
}
@GetMapping("/{id}")
fun buscar(@PathVariable id: Long): ResponseEntity<ClienteResponse> {
val cliente = clienteService.buscarPorId(id)
return ResponseEntity.ok(cliente)
}
@PostMapping
fun criar(
@Valid @RequestBody request: CriarClienteRequest
): ResponseEntity<ClienteResponse> {
val cliente = clienteService.criar(request)
val uri = URI.create("/api/v1/clientes/${cliente.id}")
return ResponseEntity.created(uri).body(cliente)
}
@PutMapping("/{id}")
fun atualizar(
@PathVariable id: Long,
@Valid @RequestBody request: AtualizarClienteRequest
): ResponseEntity<ClienteResponse> {
val cliente = clienteService.atualizar(id, request)
return ResponseEntity.ok(cliente)
}
@PatchMapping("/{id}")
fun atualizarParcial(
@PathVariable id: Long,
@RequestBody campos: Map<String, Any?>
): ResponseEntity<ClienteResponse> {
val cliente = clienteService.atualizarParcial(id, campos)
return ResponseEntity.ok(cliente)
}
@DeleteMapping("/{id}")
fun remover(@PathVariable id: Long): ResponseEntity<Void> {
clienteService.remover(id)
return ResponseEntity.noContent().build()
}
@GetMapping("/{id}/pedidos")
fun listarPedidos(
@PathVariable id: Long,
@RequestParam(defaultValue = "0") pagina: Int,
@RequestParam(defaultValue = "20") tamanho: Int
): ResponseEntity<PaginaResponse<PedidoResumoResponse>> {
val pedidos = clienteService.listarPedidos(id, pagina, tamanho)
return ResponseEntity.ok(pedidos)
}
}
Tratamento Global de Erros
Uma API profissional trata erros de forma consistente:
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(RecursoNaoEncontradoException::class)
fun handleNaoEncontrado(
ex: RecursoNaoEncontradoException,
request: HttpServletRequest
): ResponseEntity<ApiErro> {
val erro = ApiErro(
timestamp = Instant.now(),
status = 404,
erro = "Nao Encontrado",
mensagem = ex.message ?: "Recurso nao encontrado",
caminho = request.requestURI
)
return ResponseEntity.status(404).body(erro)
}
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidacao(
ex: MethodArgumentNotValidException,
request: HttpServletRequest
): ResponseEntity<ApiErro> {
val violacoes = ex.bindingResult.fieldErrors.map { fieldError ->
Violacao(
campo = fieldError.field,
mensagem = fieldError.defaultMessage ?: "Valor invalido",
valorRejeitado = fieldError.rejectedValue?.toString()
)
}
val erro = ApiErro(
timestamp = Instant.now(),
status = 400,
erro = "Erro de Validacao",
mensagem = "Um ou mais campos possuem valores invalidos",
caminho = request.requestURI,
violacoes = violacoes
)
return ResponseEntity.badRequest().body(erro)
}
@ExceptionHandler(ConflitoDeDadosException::class)
fun handleConflito(
ex: ConflitoDeDadosException,
request: HttpServletRequest
): ResponseEntity<ApiErro> {
val erro = ApiErro(
timestamp = Instant.now(),
status = 409,
erro = "Conflito",
mensagem = ex.message ?: "Conflito de dados",
caminho = request.requestURI
)
return ResponseEntity.status(409).body(erro)
}
@ExceptionHandler(Exception::class)
fun handleErroInterno(
ex: Exception,
request: HttpServletRequest
): ResponseEntity<ApiErro> {
val erro = ApiErro(
timestamp = Instant.now(),
status = 500,
erro = "Erro Interno",
mensagem = "Ocorreu um erro interno no servidor",
caminho = request.requestURI
)
return ResponseEntity.status(500).body(erro)
}
}
data class ApiErro(
val timestamp: Instant,
val status: Int,
val erro: String,
val mensagem: String,
val caminho: String,
val violacoes: List<Violacao>? = null
)
data class Violacao(
val campo: String,
val mensagem: String,
val valorRejeitado: String? = null
)
Filtragem e Busca
Implemente filtragem flexivel usando specifications do Spring Data:
@Service
class ClienteService(
private val repository: ClienteRepository
) {
fun listar(
pagina: Int,
tamanho: Int,
ordenarPor: String,
direcao: Sort.Direction,
busca: String?
): PaginaResponse<ClienteResponse> {
val pageable = PageRequest.of(pagina, tamanho, Sort.by(direcao, ordenarPor))
val resultado = if (busca.isNullOrBlank()) {
repository.findAll(pageable)
} else {
repository.buscar(busca, pageable)
}
return PaginaResponse(
conteudo = resultado.content.map { it.toResponse() },
pagina = resultado.number,
tamanho = resultado.size,
totalElementos = resultado.totalElements,
totalPaginas = resultado.totalPages,
primeira = resultado.isFirst,
ultima = resultado.isLast
)
}
}
interface ClienteRepository : JpaRepository<Cliente, Long> {
@Query("""
SELECT c FROM Cliente c
WHERE LOWER(c.nome) LIKE LOWER(CONCAT('%', :busca, '%'))
OR LOWER(c.email) LIKE LOWER(CONCAT('%', :busca, '%'))
""")
fun buscar(busca: String, pageable: Pageable): Page<Cliente>
}
Documentacao com OpenAPI
Documente sua API automaticamente com SpringDoc:
// build.gradle.kts
dependencies {
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0")
}
// Configuracao
@Configuration
class OpenApiConfig {
@Bean
fun customOpenAPI(): OpenAPI {
return OpenAPI()
.info(
Info()
.title("API de Clientes")
.version("1.0.0")
.description("API para gerenciamento de clientes")
)
.addSecurityItem(
SecurityRequirement().addList("Bearer Authentication")
)
.components(
Components().addSecuritySchemes(
"Bearer Authentication",
SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.bearerFormat("JWT")
.scheme("bearer")
)
)
}
}
Boas Praticas para REST APIs com Kotlin
- Use substantivos no plural para URLs:
/clientesem vez de/clienteou/getClientes. - Retorne codigos HTTP apropriados: 201 para criacao, 204 para delete, 400 para validacao, 404 para nao encontrado.
- Paginacao obrigatoria em listas: nunca retorne colecoes sem limite. Sempre page.
- Versionamento desde o inicio: prefixe com
/api/v1/para permitir evolucao sem quebrar clientes existentes. - Respostas consistentes: toda resposta de erro deve seguir o mesmo formato.
- HATEOAS quando fizer sentido: inclua links para recursos relacionados para facilitar a navegacao da API.
- Rate limiting: proteja sua API contra abuso com limites de requisicoes por cliente.
- Validacao na borda: valide toda entrada no controller antes de processar.
Erros Comuns e Armadilhas
- Verbos na URL:
/api/getProdutosou/api/criarClienteviolam princípios REST. Use verbos HTTP com substantivos. - Retornar 200 para erros: uma resposta com
{"erro": "nao encontrado"}e status 200 confunde clientes. Use o codigo HTTP correto. - Expor entidades internas: retornar a entidade JPA diretamente na resposta expoe detalhes internos e pode causar problemas de serializacao com lazy loading.
- Ignorar idempotencia: PUT e DELETE devem ser idempotentes. Chamar DELETE duas vezes no mesmo recurso nao deve causar erro.
- Listas sem paginacao: retornar milhares de registros em uma unica resposta causa problemas de performance e memoria.
- Falta de documentacao: uma API sem documentacao e como uma biblioteca sem indice. Use OpenAPI/Swagger.
Conclusao e Proximos Passos
Construir REST APIs profissionais com Kotlin vai alem de criar endpoints que funcionam. Envolve design cuidadoso de recursos, tratamento consistente de erros, validacao robusta, documentacao clara e atencao a performance. Com as praticas apresentadas neste guia, voce tem a base para criar APIs que sao um prazer de consumir. Explore nossos guias sobre testes para garantir a qualidade da API, autenticacao com JWT para seguranca e microservicos para arquiteturas mais complexas.