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: /clientes em vez de /cliente ou /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/getProdutos ou /api/criarCliente violam 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.