---
title: "Kotlin com Spring Boot: Tutorial Passo a Passo | Kotlin Brasil"
url: "https://kotlin.dev.br/blog/kotlin-spring-boot/"
markdown_url: "https://kotlin.dev.br/blog/kotlin-spring-boot.MD"
description: "Aprenda a criar APIs REST com Kotlin e Spring Boot. Tutorial passo a passo em português com exemplos completos e boas práticas."
date: "2026-03-08"
author: "Karina Melo"
---

# Kotlin com Spring Boot: Tutorial Passo a Passo | Kotlin Brasil

Aprenda a criar APIs REST com Kotlin e Spring Boot. Tutorial passo a passo em português com exemplos completos e boas práticas.


Spring Boot é o framework mais popular do ecossistema Java para backend, e ele funciona maravilhosamente bem com Kotlin. Na verdade, a combinação Kotlin + Spring Boot é tão boa que a equipe do Spring tem suporte oficial e dedicado à linguagem. Vamos construir uma API REST do zero neste tutorial.

## Por que Kotlin com Spring Boot?

A pergunta melhor seria: por que não? Kotlin traz benefícios concretos para projetos Spring Boot:

- **Menos boilerplate**: data classes, default parameters, extension functions
- **Null Safety**: menos bugs em runtime
- **Coroutines**: suporte nativo para programação reativa com WebFlux
- **DSLs**: configurações declarativas e elegantes
- **Suporte oficial**: a equipe do Spring mantém integração de primeira classe

## Criando o projeto

Acesse o [Spring Initializr](https://start.spring.io) e selecione:

- **Language**: Kotlin
- **Build tool**: Gradle - Kotlin
- **Dependencies**: Spring Web, Spring Data JPA, H2 Database, Spring Validation

Ou configure manualmente o `build.gradle.kts`:

```kotlin
plugins {
    id("org.springframework.boot") version "3.4.0"
    id("io.spring.dependency-management") version "1.1.6"
    kotlin("jvm") version "2.1.0"
    kotlin("plugin.spring") version "2.1.0"
    kotlin("plugin.jpa") version "2.1.0"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    runtimeOnly("com.h2database:h2")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}
```

Os plugins `plugin.spring` e `plugin.jpa` são essenciais: eles geram construtores sem argumentos e abrem classes automaticamente (Spring precisa disso para proxies).

## Definindo a entidade

```kotlin
@Entity
@Table(name = "produtos")
data class Produto(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @field:NotBlank(message = "Nome e obrigatorio")
    val nome: String,

    @field:Size(max = 500, message = "Descrição deve ter no máximo 500 caracteres")
    val descricao: String = "",

    @field:Positive(message = "Preço deve ser positivo")
    val preco: BigDecimal,

    @field:PositiveOrZero(message = "Estoque nao pode ser negativo")
    val estoque: Int = 0,

    val ativo: Boolean = true
)
```

Repare nos valores padrão — isso elimina a necessidade de múltiplos construtores.

## Repositório

Com Spring Data JPA, o repositório é simples assim:

```kotlin
@Repository
interface ProdutoRepository : JpaRepository<Produto, Long> {
    fun findByAtivoTrue(): List<Produto>
    fun findByNomeContainingIgnoreCase(nome: String): List<Produto>
    fun findByPrecoLessThanEqual(precoMaximo: BigDecimal): List<Produto>
}
```

Sem implementação necessária — o Spring gera tudo automaticamente a partir dos nomes dos métodos.

## DTOs com data class

Use data classes para separar a camada de transporte da entidade:

```kotlin
data class ProdutoRequest(
    @field:NotBlank(message = "Nome e obrigatorio")
    val nome: String,
    val descricao: String = "",
    @field:Positive(message = "Preço deve ser positivo")
    val preco: BigDecimal,
    val estoque: Int = 0
)

data class ProdutoResponse(
    val id: Long,
    val nome: String,
    val descricao: String,
    val preco: BigDecimal,
    val estoque: Int,
    val ativo: Boolean
)

fun Produto.toResponse() = ProdutoResponse(
    id = id,
    nome = nome,
    descricao = descricao,
    preco = preco,
    estoque = estoque,
    ativo = ativo
)
```

A extension function `toResponse()` é o jeito Kotlin de fazer mapeamento entre entidade e DTO sem precisar de bibliotecas como MapStruct.

## Service

```kotlin
@Service
class ProdutoService(private val repository: ProdutoRepository) {

    fun listarTodos(): List<ProdutoResponse> =
        repository.findByAtivoTrue().map { it.toResponse() }

    fun buscarPorId(id: Long): ProdutoResponse =
        repository.findByIdOrNull(id)?.toResponse()
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Produto nao encontrado")

    fun buscarPorNome(nome: String): List<ProdutoResponse> =
        repository.findByNomeContainingIgnoreCase(nome).map { it.toResponse() }

    @Transactional
    fun criar(request: ProdutoRequest): ProdutoResponse {
        val produto = Produto(
            nome = request.nome,
            descricao = request.descricao,
            preco = request.preco,
            estoque = request.estoque
        )
        return repository.save(produto).toResponse()
    }

    @Transactional
    fun atualizar(id: Long, request: ProdutoRequest): ProdutoResponse {
        val existente = repository.findByIdOrNull(id)
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Produto nao encontrado")

        val atualizado = existente.copy(
            nome = request.nome,
            descricao = request.descricao,
            preco = request.preco,
            estoque = request.estoque
        )
        return repository.save(atualizado).toResponse()
    }

    @Transactional
    fun deletar(id: Long) {
        val produto = repository.findByIdOrNull(id)
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Produto nao encontrado")
        repository.save(produto.copy(ativo = false)) // soft delete
    }
}
```

Veja como o `copy()` da data class facilita a atualização: criamos uma nova instância alterando só o que mudou.

## Controller

```kotlin
@RestController
@RequestMapping("/api/produtos")
class ProdutoController(private val service: ProdutoService) {

    @GetMapping
    fun listarTodos() = service.listarTodos()

    @GetMapping("/{id}")
    fun buscarPorId(@PathVariable id: Long) = service.buscarPorId(id)

    @GetMapping("/busca")
    fun buscarPorNome(@RequestParam nome: String) = service.buscarPorNome(nome)

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun criar(@Valid @RequestBody request: ProdutoRequest) = service.criar(request)

    @PutMapping("/{id}")
    fun atualizar(
        @PathVariable id: Long,
        @Valid @RequestBody request: ProdutoRequest
    ) = service.atualizar(id, request)

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    fun deletar(@PathVariable id: Long) = service.deletar(id)
}
```

## Tratamento global de erros

```kotlin
@RestControllerAdvice
class GlobalExceptionHandler {

    data class ErroResponse(
        val status: Int,
        val mensagem: String,
        val timestamp: LocalDateTime = LocalDateTime.now()
    )

    @ExceptionHandler(ResponseStatusException::class)
    fun handleResponseStatus(ex: ResponseStatusException): ResponseEntity<ErroResponse> {
        val erro = ErroResponse(
            status = ex.statusCode.value(),
            mensagem = ex.reason ?: "Erro desconhecido"
        )
        return ResponseEntity.status(ex.statusCode).body(erro)
    }

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ErroResponse> {
        val mensagens = ex.bindingResult.fieldErrors.joinToString("; ") {
            "${it.field}: ${it.defaultMessage}"
        }
        val erro = ErroResponse(status = 400, mensagem = mensagens)
        return ResponseEntity.badRequest().body(erro)
    }
}
```

## Testando a API

Com Spring Boot Test e Kotlin, os testes ficam bem expressivos:

```kotlin
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProdutoControllerTest(@Autowired val restTemplate: TestRestTemplate) {

    @Test
    fun `deve criar produto com sucesso`() {
        val request = ProdutoRequest(
            nome = "Teclado Mecânico",
            descricao = "Switch Cherry MX Blue",
            preco = BigDecimal("349.90"),
            estoque = 50
        )

        val response = restTemplate.postForEntity(
            "/api/produtos",
            request,
            ProdutoResponse::class.java
        )

        assertThat(response.statusCode).isEqualTo(HttpStatus.CREATED)
        assertThat(response.body?.nome).isEqualTo("Teclado Mecânico")
        assertThat(response.body?.id).isGreaterThan(0)
    }
}
```

Kotlin permite usar crases nos nomes dos testes, o que deixa a descrição muito mais legível.

## Conclusão

Kotlin e Spring Boot formam uma dupla espetacular para backend. Você tem toda a maturidade e estabilidade do ecossistema Spring com a expressividade e segurança de Kotlin. Se você já conhece Spring com Java, a transição para Kotlin é suavíssima — e os ganhos em produtividade aparecem logo nas primeiras linhas de código. Para comparar com outras abordagens de backend, veja como <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go constrói APIs REST com net/http e Gin</a> ou como <a href="https://python.dev.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">Python faz o mesmo com FastAPI e Django</a>.

Monta seu projeto e bora codar!
