Kotlin e Spring Boot formam uma combinacao poderosa para desenvolvimento backend na JVM. O Spring oferece suporte oficial a Kotlin desde a versao 5, com extensoes dedicadas que aproveitam recursos como null safety, data classes, extension functions e coroutines. Muitas empresas que ja usam Spring estao migrando de Java para Kotlin no backend, beneficiando-se de um codigo mais conciso e seguro sem abrir mao do vasto ecossistema Spring. Este guia cobre desde a configuracao inicial ate padroes avancados para aplicacoes em producao.
Configurando o Projeto
O Spring Initializr (start.spring.io) permite gerar projetos Kotlin com Spring Boot. A configuracao no build.gradle.kts inclui plugins especificos:
plugins {
id("org.springframework.boot") version "3.2.2"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.22"
kotlin("plugin.spring") version "1.9.22"
kotlin("plugin.jpa") version "1.9.22"
}
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")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
O plugin kotlin-spring adiciona automaticamente o modificador open as classes anotadas com @Component, @Service, @Repository e @Configuration, necessario para os proxies do Spring. O plugin kotlin-jpa gera construtores no-arg para entidades JPA.
Classe Principal da Aplicacao
@SpringBootApplication
class MeuAppApplication
fun main(args: Array<String>) {
runApplication<MeuAppApplication>(*args)
}
A funcao runApplication e uma extension function do Spring para Kotlin, substituindo o padrao Java SpringApplication.run().
Entidades JPA com Kotlin
Data classes nao sao ideais para entidades JPA, pois o JPA requer mutabilidade e proxies. A abordagem recomendada:
@Entity
@Table(name = "produtos")
class Produto(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
@Column(nullable = false)
var nome: String,
@Column(nullable = false)
var preco: BigDecimal,
@Column(length = 1000)
var descricao: String? = null,
@Column(name = "criado_em")
val criadoEm: LocalDateTime = LocalDateTime.now(),
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "categoria_id")
var categoria: Categoria? = null
)
@Entity
@Table(name = "categorias")
class Categoria(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
@Column(nullable = false, unique = true)
var nome: String,
@OneToMany(mappedBy = "categoria", cascade = [CascadeType.ALL])
val produtos: MutableList<Produto> = mutableListOf()
)
Repositories com Spring Data
O Spring Data JPA gera implementacoes automaticamente a partir de interfaces:
interface ProdutoRepository : JpaRepository<Produto, Long> {
fun findByNomeContainingIgnoreCase(nome: String): List<Produto>
fun findByCategoriaId(categoriaId: Long): List<Produto>
@Query("SELECT p FROM Produto p WHERE p.preco BETWEEN :min AND :max")
fun findByFaixaDePreco(
@Param("min") min: BigDecimal,
@Param("max") max: BigDecimal
): List<Produto>
fun findByNomeContainingIgnoreCase(
nome: String,
pageable: Pageable
): Page<Produto>
}
Camada de Servico
A camada de servico contem a logica de negocio e coordena operacoes entre repositories:
@Service
class ProdutoService(
private val produtoRepository: ProdutoRepository,
private val categoriaRepository: CategoriaRepository
) {
fun listarTodos(pageable: Pageable): Page<ProdutoResponse> {
return produtoRepository.findAll(pageable).map { it.toResponse() }
}
fun buscarPorId(id: Long): ProdutoResponse {
val produto = produtoRepository.findById(id)
.orElseThrow { RecursoNaoEncontradoException("Produto $id nao encontrado") }
return produto.toResponse()
}
@Transactional
fun criar(request: CriarProdutoRequest): ProdutoResponse {
val categoria = request.categoriaId?.let {
categoriaRepository.findById(it)
.orElseThrow { RecursoNaoEncontradoException("Categoria $it nao encontrada") }
}
val produto = Produto(
nome = request.nome,
preco = request.preco,
descricao = request.descricao,
categoria = categoria
)
return produtoRepository.save(produto).toResponse()
}
@Transactional
fun atualizar(id: Long, request: AtualizarProdutoRequest): ProdutoResponse {
val produto = produtoRepository.findById(id)
.orElseThrow { RecursoNaoEncontradoException("Produto $id nao encontrado") }
request.nome?.let { produto.nome = it }
request.preco?.let { produto.preco = it }
request.descricao?.let { produto.descricao = it }
return produtoRepository.save(produto).toResponse()
}
@Transactional
fun deletar(id: Long) {
if (!produtoRepository.existsById(id)) {
throw RecursoNaoEncontradoException("Produto $id nao encontrado")
}
produtoRepository.deleteById(id)
}
}
DTOs e Mapeamento
Data classes sao perfeitas para DTOs de request e response:
data class CriarProdutoRequest(
@field:NotBlank(message = "Nome e obrigatorio")
val nome: String,
@field:Positive(message = "Preco deve ser positivo")
val preco: BigDecimal,
val descricao: String? = null,
val categoriaId: Long? = null
)
data class AtualizarProdutoRequest(
val nome: String? = null,
val preco: BigDecimal? = null,
val descricao: String? = null
)
data class ProdutoResponse(
val id: Long,
val nome: String,
val preco: BigDecimal,
val descricao: String?,
val categoriaNome: String?,
val criadoEm: LocalDateTime
)
// Extension function para mapeamento
fun Produto.toResponse() = ProdutoResponse(
id = id,
nome = nome,
preco = preco,
descricao = descricao,
categoriaNome = categoria?.nome,
criadoEm = criadoEm
)
REST Controllers
@RestController
@RequestMapping("/api/produtos")
class ProdutoController(
private val produtoService: ProdutoService
) {
@GetMapping
fun listar(
@RequestParam(defaultValue = "0") pagina: Int,
@RequestParam(defaultValue = "20") tamanho: Int,
@RequestParam(defaultValue = "nome") ordenarPor: String
): Page<ProdutoResponse> {
val pageable = PageRequest.of(pagina, tamanho, Sort.by(ordenarPor))
return produtoService.listarTodos(pageable)
}
@GetMapping("/{id}")
fun buscar(@PathVariable id: Long): ProdutoResponse {
return produtoService.buscarPorId(id)
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun criar(@Valid @RequestBody request: CriarProdutoRequest): ProdutoResponse {
return produtoService.criar(request)
}
@PutMapping("/{id}")
fun atualizar(
@PathVariable id: Long,
@Valid @RequestBody request: AtualizarProdutoRequest
): ProdutoResponse {
return produtoService.atualizar(id, request)
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deletar(@PathVariable id: Long) {
produtoService.deletar(id)
}
}
Tratamento Global de Erros
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(RecursoNaoEncontradoException::class)
@ResponseStatus(HttpStatus.NOT_FOUND)
fun handleRecursoNaoEncontrado(ex: RecursoNaoEncontradoException): ErroResponse {
return ErroResponse(
status = 404,
mensagem = ex.message ?: "Recurso nao encontrado",
timestamp = LocalDateTime.now()
)
}
@ExceptionHandler(MethodArgumentNotValidException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun handleValidacao(ex: MethodArgumentNotValidException): ErroResponse {
val erros = ex.bindingResult.fieldErrors.associate {
it.field to (it.defaultMessage ?: "Valor invalido")
}
return ErroResponse(
status = 400,
mensagem = "Erro de validacao",
detalhes = erros,
timestamp = LocalDateTime.now()
)
}
}
data class ErroResponse(
val status: Int,
val mensagem: String,
val detalhes: Map<String, String>? = null,
val timestamp: LocalDateTime
)
class RecursoNaoEncontradoException(message: String) : RuntimeException(message)
Boas Praticas para Kotlin com Spring Boot
- Use o plugin kotlin-spring: ele evita a necessidade de abrir classes manualmente para proxies do Spring.
- Prefira constructor injection: o Kotlin facilita isso com parametros no construtor primario. Evite
@Autowiredem campos. - Nao use data classes para entidades JPA: data classes geram
equals/hashCodebaseados em todos os campos, o que e problematico com lazy loading. - Aproveite null safety: use tipos nullable do Kotlin para campos opcionais em vez de
Optionaldo Java. - Use extension functions para mapeamento: elas sao mais limpas que classes Mapper separadas para conversao entre entidades e DTOs.
- Configure o Jackson corretamente: o modulo
jackson-module-kotline essencial para serializar/deserializar data classes corretamente.
Erros Comuns e Armadilhas
- Esquecer o plugin kotlin-jpa: sem ele, entidades JPA sem construtor no-arg causam erros em runtime.
- Data classes como entidades: o
equalsehashCodegerados automaticamente causam problemas com proxies lazy do Hibernate. - Nao registrar o modulo Kotlin do Jackson: sem o
jackson-module-kotlin, data classes nao sao deserializadas corretamente, resultando em erros confusos. - Usar
lateinitsem necessidade: em Spring, constructor injection elimina a necessidade delateinit varna maioria dos casos. - Ignorar paginacao: retornar listas completas em endpoints sem paginacao pode causar problemas serios de performance com tabelas grandes.
Conclusao e Proximos Passos
Kotlin com Spring Boot oferece uma experiencia de desenvolvimento backend produtiva e robusta. A combinacao do ecossistema maduro do Spring com a expressividade do Kotlin resulta em aplicacoes mais seguras e concisas. Para ir alem, explore Spring Security para autenticacao, WebFlux com coroutines para aplicacoes reativas e consulte nossos guias sobre testes, Docker e microservicos para completar sua stack de desenvolvimento.