---
title: "Spring Boot com Kotlin Tutorial em Português — Passo a Passo | Kotlin Brasil"
url: "https://kotlin.dev.br/tutoriais/kotlin-spring-boot-tutorial/"
markdown_url: "https://kotlin.dev.br/tutoriais/kotlin-spring-boot-tutorial.MD"
description: "Aprenda Spring Boot com Kotlin: REST controllers, services, Spring Data JPA, coroutines no Spring e configurações específicas para Kotlin."
date: "2025-07-15"
author: "Karina Melo"
---

# Spring Boot com Kotlin Tutorial em Português — Passo a Passo | Kotlin Brasil

Aprenda Spring Boot com Kotlin: REST controllers, services, Spring Data JPA, coroutines no Spring e configurações específicas para Kotlin.


Neste tutorial completo, você vai aprender a criar uma aplicação **Spring Boot com Kotlin** do zero. Vamos cobrir a configuração do projeto, REST controllers, services, repositórios com Spring Data JPA, funcionalidades específicas do Kotlin no Spring, integração com [Coroutines](/glossario/coroutine/) e configuração avançada. Kotlin é uma linguagem oficialmente suportada pelo Spring e oferece uma experiência de desenvolvimento mais concisa e segura que o Java.

## Por que usar Kotlin com Spring Boot?

O Spring Framework oferece suporte oficial ao Kotlin desde a versão 5.0, é o Spring Boot desde a versão 2.0. As vantagens incluem:

- **Null safety**: o sistema de tipos do Kotlin com [nullable](/glossario/nullable/) elimina NullPointerExceptions em tempo de compilação
- **Data classes**: perfeitas para DTOs e entidades, eliminando getters, setters, equals, hashCode e toString
- **Extension functions**: permitem estender classes do Spring de forma elegante
- **Coroutines**: suporte nativo para programação assíncrona não-bloqueante
- **Código conciso**: menos boilerplate que Java, mantendo a mesma funcionalidade

## Passo 1: Criando o Projeto

A maneira mais fácil de criar um projeto Spring Boot com Kotlin é usando o [Spring Initializr](https://start.spring.io). Selecione:

- **Language**: Kotlin
- **Build**: Gradle - Kotlin
- **Dependencies**: Spring Web, Spring Data JPA, H2 Database (ou PostgreSQL), Spring Validation

O `build.gradle.kts` deve incluir os plugins e dependências essenciais:

```kotlin
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "3.2.3"
    id("io.spring.dependency-management") version "1.1.4"
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.spring") version "1.9.22"  // abre classes para proxying
    kotlin("plugin.jpa") version "1.9.22"     // gera construtor sem args para JPA
}

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")
    implementation("org.jetbrains.kotlin:kotlin-reflect")

    // Coroutines (opcional)
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")

    runtimeOnly("com.h2database:h2") // banco em memoria para desenvolvimento
    // runtimeOnly("org.postgresql:postgresql") // para produção

    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs += "-Xjsr305=strict" // null safety para anotações Java
        jvmTarget = "17"
    }
}
```

Os plugins `plugin.spring` e `plugin.jpa` são essenciais: o primeiro adiciona o modificador `open` automaticamente às classes com anotações Spring (já que o Kotlin cria classes `final` por padrão), e o segundo gera construtores sem argumentos necessários pelo JPA.

## Passo 2: Configurando a Aplicação

O ponto de entrada da aplicação:

```kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}
```

Note que usamos `runApplication` — uma [extension function](/glossario/extension-function/) do Spring para Kotlin que simplifica a inicialização. O `application.yml` (ou `application.properties`) configura o ambiente:

```kotlin
// application.yml
// spring:
//   datasource:
//     url: jdbc:h2:mem:testdb
//     driver-class-name: org.h2.Driver
//   jpa:
//     hibernate:
//       ddl-auto: update
//     show-sql: true
//   h2:
//     console:
//       enabled: true
// server:
//   port: 8080
```

## Passo 3: Criando a Entity com JPA

Com Kotlin, usamos [data classes](/glossario/data-class/) para entidades, mas com algumas considerações especiais para o JPA:

```kotlin
import jakarta.persistence.*
import java.time.LocalDateTime

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

    @Column(nullable = false, length = 100)
    val nome: String,

    @Column(length = 500)
    val descricao: String = "",

    @Column(nullable = false)
    val preco: Double,

    @Column(name = "em_estoque")
    val emEstoque: Boolean = true,

    @Column(name = "criado_em")
    val criadoEm: LocalDateTime = LocalDateTime.now()
)
```

O plugin `kotlin-jpa` gera o construtor sem argumentos que o Hibernate precisa. Os valores padrão nos parâmetros permitem criar instâncias parciais sem precisar de builders.

Para DTOs de entrada e saída, usamos data classes simples com válidação:

```kotlin
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Positive
import jakarta.validation.constraints.Size

data class ProdutoRequest(
    @field:NotBlank(message = "Nome e obrigatorio")
    @field:Size(min = 2, max = 100, message = "Nome deve ter entre 2 e 100 caracteres")
    val nome: String,

    val descricao: String = "",

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

data class ProdutoResponse(
    val id: Long,
    val nome: String,
    val descricao: String,
    val preco: Double,
    val emEstoque: Boolean,
    val criadoEm: LocalDateTime
) {
    companion object {
        fun fromEntity(produto: Produto) = ProdutoResponse(
            id = produto.id,
            nome = produto.nome,
            descricao = produto.descricao,
            preco = produto.preco,
            emEstoque = produto.emEstoque,
            criadoEm = produto.criadoEm
        )
    }
}
```

Note o uso de `@field:` antes das anotações de válidação — isso é necessário em Kotlin para que as anotações sejam aplicadas ao campo Java subjacente e não ao parâmetro do construtor.

## Passo 4: Criando o Repository com Spring Data JPA

O Spring Data JPA gera a implementação do repositório automaticamente a partir da [interface](/glossario/interface/):

```kotlin
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository

@Repository
interface ProdutoRepository : JpaRepository<Produto, Long> {

    // Query derivada do nome do metodo
    fun findByNomeContainingIgnoreCase(nome: String): List<Produto>

    fun findByEmEstoque(emEstoque: Boolean): List<Produto>

    fun findByPrecoLessThanEqual(precoMaximo: Double): List<Produto>

    // Query JPQL personalizada
    @Query("SELECT p FROM Produto p WHERE p.preco BETWEEN :min AND :max ORDER BY p.preco")
    fun findByFaixaDePreco(min: Double, max: Double): List<Produto>

    // Query nativa
    @Query(
        value = "SELECT * FROM produtos WHERE em_estoque = true ORDER BY criado_em DESC LIMIT :limite",
        nativeQuery = true
    )
    fun findRecentes(limite: Int): List<Produto>

    fun countByEmEstoque(emEstoque: Boolean): Long
}
```

O Spring Data interpreta o nome do método e gera a query SQL automaticamente. Para consultas mais complexas, use `@Query` com JPQL ou SQL nativo.

## Passo 5: Criando o Service

A camada de serviço contém a lógica de negócios:

```kotlin
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

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

    fun listarTodos(): List<ProdutoResponse> {
        return repository.findAll().map { ProdutoResponse.fromEntity(it) }
    }

    fun buscarPorId(id: Long): ProdutoResponse {
        val produto = repository.findById(id)
            .orElseThrow { ProdutoNaoEncontradoException(id) }
        return ProdutoResponse.fromEntity(produto)
    }

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

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

    @Transactional
    fun atualizar(id: Long, request: ProdutoRequest): ProdutoResponse {
        val existente = repository.findById(id)
            .orElseThrow { ProdutoNaoEncontradoException(id) }

        val atualizado = existente.copy(
            nome = request.nome,
            descricao = request.descricao,
            preco = request.preco
        )
        val salvo = repository.save(atualizado)
        return ProdutoResponse.fromEntity(salvo)
    }

    @Transactional
    fun deletar(id: Long) {
        if (!repository.existsById(id)) {
            throw ProdutoNaoEncontradoException(id)
        }
        repository.deleteById(id)
    }
}

class ProdutoNaoEncontradoException(id: Long) :
    RuntimeException("Produto com ID $id nao encontrado")
```

Note como o Kotlin torna o código mais limpo: injeção de dependência pelo construtor, `copy()` para atualização parcial, e string templates para mensagens de erro.

## Passo 6: Criando o REST Controller

O controller expõe os endpoints da API:

```kotlin
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

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

    @GetMapping
    fun listarTodos(): ResponseEntity<List<ProdutoResponse>> {
        return ResponseEntity.ok(service.listarTodos())
    }

    @GetMapping("/{id}")
    fun buscarPorId(@PathVariable id: Long): ResponseEntity<ProdutoResponse> {
        return ResponseEntity.ok(service.buscarPorId(id))
    }

    @GetMapping("/buscar")
    fun buscarPorNome(@RequestParam nome: String): ResponseEntity<List<ProdutoResponse>> {
        return ResponseEntity.ok(service.buscarPorNome(nome))
    }

    @PostMapping
    fun criar(@Valid @RequestBody request: ProdutoRequest): ResponseEntity<ProdutoResponse> {
        val produto = service.criar(request)
        return ResponseEntity.status(HttpStatus.CREATED).body(produto)
    }

    @PutMapping("/{id}")
    fun atualizar(
        @PathVariable id: Long,
        @Valid @RequestBody request: ProdutoRequest
    ): ResponseEntity<ProdutoResponse> {
        return ResponseEntity.ok(service.atualizar(id, request))
    }

    @DeleteMapping("/{id}")
    fun deletar(@PathVariable id: Long): ResponseEntity<Unit> {
        service.deletar(id)
        return ResponseEntity.noContent().build()
    }
}
```

## Passo 7: Tratamento Global de Erros

Use `@ControllerAdvice` para tratar exceções de forma centralizada:

```kotlin
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler

data class ErroResponse(
    val status: Int,
    val mensagem: String,
    val erros: List<String> = emptyList()
)

@ControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(ProdutoNaoEncontradoException::class)
    fun handleNaoEncontrado(ex: ProdutoNaoEncontradoException): ResponseEntity<ErroResponse> {
        val erro = ErroResponse(
            status = HttpStatus.NOT_FOUND.value(),
            mensagem = ex.message ?: "Recurso nao encontrado"
        )
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(erro)
    }

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidacao(ex: MethodArgumentNotValidException): ResponseEntity<ErroResponse> {
        val erros = ex.bindingResult.fieldErrors.map { "${it.field}: ${it.defaultMessage}" }
        val erro = ErroResponse(
            status = HttpStatus.BAD_REQUEST.value(),
            mensagem = "Erro de validação",
            erros = erros
        )
        return ResponseEntity.badRequest().body(erro)
    }

    @ExceptionHandler(Exception::class)
    fun handleGenerico(ex: Exception): ResponseEntity<ErroResponse> {
        val erro = ErroResponse(
            status = HttpStatus.INTERNAL_SERVER_ERROR.value(),
            mensagem = "Erro interno do servidor"
        )
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(erro)
    }
}
```

## Passo 8: Coroutines no Spring

O Spring Boot suporta [Coroutines](/glossario/coroutine/) nativamente com o módulo WebFlux. Você pode usar funções [suspend](/glossario/suspend/) e [Flow](/glossario/flow/) diretamente nos controllers:

```kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/async")
class AsyncController(
    private val service: AsyncService
) {

    // Endpoint com suspend function
    @GetMapping("/produto/{id}")
    suspend fun buscarProduto(@PathVariable id: Long): ProdutoResponse {
        return service.buscarProdutoAsync(id)
    }

    // Endpoint que retorna Flow (streaming)
    @GetMapping("/stream")
    fun streamProdutos(): Flow<ProdutoResponse> = flow {
        val produtos = service.listarTodosAsync()
        produtos.forEach { produto ->
            delay(100) // simula processamento
            emit(produto)
        }
    }
}

@Service
class AsyncService(
    private val repository: ProdutoRepository
) {

    suspend fun buscarProdutoAsync(id: Long): ProdutoResponse {
        // Em aplicacoes reais, use R2DBC para acesso nao-bloqueante ao banco
        val produto = repository.findById(id)
            .orElseThrow { ProdutoNaoEncontradoException(id) }
        return ProdutoResponse.fromEntity(produto)
    }

    suspend fun listarTodosAsync(): List<ProdutoResponse> {
        return repository.findAll().map { ProdutoResponse.fromEntity(it) }
    }
}
```

Para aproveitar ao máximo as coroutines no Spring, considere usar **Spring WebFlux** com **R2DBC** em vez do Spring MVC com JPA, pois o JPA tradicional é bloqueante por natureza.

## Passo 9: Funcionalidades Kotlin-Specific no Spring

O Spring oferece várias extensões para Kotlin que tornam o código mais idiomático:

```kotlin
import org.springframework.beans.factory.getBean
import org.springframework.context.support.beans

// Bean definition DSL — alternativa a @Configuration
val beans = beans {
    bean<ProdutoService>()
    bean {
        ProdutoController(ref())
    }
}

// Router DSL — alternativa funcional aos controllers
import org.springframework.web.servlet.function.router

fun produtoRoutes(service: ProdutoService) = router {
    "/api/produtos".nest {
        GET("") { _ ->
            ok().body(service.listarTodos())
        }
        GET("/{id}") { request ->
            val id = request.pathVariable("id").toLong()
            ok().body(service.buscarPorId(id))
        }
        POST("") { request ->
            val body = request.body(ProdutoRequest::class.java)
            status(HttpStatus.CREATED).body(service.criar(body))
        }
    }
}
```

Essas DSLs são opcionais e oferecem uma abordagem funcional como alternativa às anotações tradicionais.

## Erros Comuns

1. **Esquecer o plugin `kotlin-spring`**: Sem esse plugin, as classes Kotlin são `final` por padrão, e o Spring não consegue criar proxies para injeção de dependência e transações. O plugin adiciona `open` automaticamente.

2. **Não usar `@field:` nas anotações de válidação**: Em Kotlin, anotações em propriedades do construtor primário precisam do target `@field:` para que as válidações do Bean Validation funcionem corretamente.

3. **Usar `var` nas Entities sem necessidade**: Prefira [val](/glossario/val/) e use `copy()` para criar versões modificadas. Entidades mutáveis são fonte de bugs difíceis de rastrear.

4. **Ignorar o `-Xjsr305=strict`**: Essa flag do compilador faz com que as anotações de nulidade do Java (como `@Nullable` e `@NonNull` do Spring) sejam respeitadas pelo tipo do Kotlin, melhorando a segurança de tipos.

5. **Misturar JPA bloqueante com coroutines sem cuidado**: Usar `suspend` no controller com JPA tradicional não torna o acesso ao banco não-bloqueante. Para I/O verdadeiramente assíncrono, use R2DBC.

## Conclusão e Próximos Passos

Neste tutorial, você aprendeu a criar uma aplicação Spring Boot completa com Kotlin: configuração do projeto com plugins essenciais, entidades JPA com [data classes](/glossario/data-class/), repositórios com Spring Data JPA, services com lógica de negócios, REST controllers com válidação, tratamento global de erros, [coroutines](/glossario/coroutine/) no Spring, e funcionalidades específicas do Kotlin como Router DSL e Bean DSL.

Como próximos passos, recomendamos:

- Explorar Spring Security com Kotlin para autenticação e autorização
- Estudar R2DBC para acesso não-bloqueante ao banco de dados com coroutines
- Aprender sobre testes com MockK (alternativa Kotlin-friendly ao Mockito)
- Implementar documentação da API com SpringDoc/Swagger
- Consultar o [glossário de extension function](/glossario/extension-function/) e [interface](/glossario/interface/) para reforçar conceitos
- Explorar o [Kotlin Flow](/tutoriais/kotlin-flow-tutorial/) para streaming de dados no backend

Spring Boot com Kotlin é uma combinação poderosa para desenvolvimento backend, oferecendo produtividade, segurança de tipos e todo o ecossistema maduro do Spring Framework.

Se você quer comparar frameworks backend em outras linguagens, <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go com Gin e Echo oferece APIs leves e performáticas</a>, enquanto <a href="https://python.dev.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">Python com Django e FastAPI domina em prototipagem rápida</a>.
