Fala pessoal! neste artigo, vamos criar uma API RESTful utilizando Kotlin e o framework Spring Boot. Vamos abordar desde a configuração inicial do projeto até a implementação de endpoints para realizar operações CRUD (Create, Read, Update, Delete) em uma entidade simples.
Kotlin e Spring Boot são duas tecnologias que, quando combinadas, oferecem um ambiente poderoso e eficiente para o desenvolvimento de APIs REST.
Kotlin, uma linguagem de programação moderna e concisa, tem ganhado popularidade devido à sua interoperabilidade com o Java e sua sintaxe limpa. Por outro lado, o Spring Boot, um framework amplamente utilizado para desenvolvimento rápido de aplicativos Java, oferece recursos robustos e uma configuração mínima.
Configuração do Projeto
Para criar um novo projeto Spring Boot com Kotlin, podemos utilizar o Spring Initializr ou criar um projeto manualmente.
Vamos utilizar o Spring Initializr para facilitar o processo. Acesse https://start.spring.io/ e configure o projeto conforme abaixo:
- Project: Maven Project
- Language: Kotlin
- Spring Boot: Escolha a versão mais recente
- Project Metadata:
- Dependencies: Adicione Spring Web e Spring Data JPA
- Artifact: br.com.virandoprogramador
- Name: demo
- Description: Kotlin com Spring Boot
Clique em “Generate” para baixar o arquivo zip contendo o projeto inicial.
No arquivo pom.xml vão estar as seguintes dependências:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-reflect</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jdk8</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>
Agora precisamos adicionar algumas configurações de conexão com o banco de dados, em nosso caso vamos utilizar o banco H2 apenas para teste.
Adicione as seguintes configurações para seu banco de dados, no arquivo properties.yml:
spring: datasource: driverClassName: org.h2.Driver url: jdbc:h2:mem:forum username: sa password: jpa: database-platform: org.hibernate.dialect.H2Dialect
Após adicionarmos as dependências necessárias, agora vamos começar a criar as classes para a criação da nossa Api Rest.
Classes do Projeto Spring Boot e Kotlin
Para iniciar, a nossa classe principal do Spring Boot ficou da seguinte maneira:
package br.com.virandoprogramador import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class ForumApplication fun main(args: Array<String>) { runApplication<ForumApplication>(*args) }
Após rodar nossa aplicação, vamos começar a criar as classes do projeto.
Primeiro, vamos criar as classes de Entidade (Modelo), em nosso exemplo, vamos criar 5 entidades que são: Curso, Respostas, StatusTopico, Tópico e Usuario:
Package: Model:
Entidade Curso:
package br.com.virandoprogramador.model import javax.persistence.* @Entity data class Curso ( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, val nome: String, val categoria: String )
Entidade Respostas:
package br.com.virandoprogramador.model import java.time.LocalDateTime import javax.persistence.* @Entity data class Respostas ( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long?, val mensagem: String, val dataCriacao: LocalDateTime = LocalDateTime.now(), @ManyToOne val autor: Usuario, @ManyToOne val topico: Topico, val solucao: Boolean )
Entidade StatusTopico:
package br.com.virandoprogramador.model enum class StatusTopico { NAO_RESPONDIDO, NAO_SOLUCIONADO, SOLUCIONADO, FECHADO }
Entidade Topico:
package br.com.virandoprogramador.model import java.time.LocalDateTime import javax.persistence.* @Entity data class Topico( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, var titulo: String, var mensagem: String, val dataCriacao: LocalDateTime = LocalDateTime.now(), @OneToMany(mappedBy = "topico") val respostas: List<Respostas> = ArrayList(), @Enumerated(value = EnumType.STRING) val status: StatusTopico = StatusTopico.NAO_RESPONDIDO, @ManyToOne val curso: Curso, @ManyToOne val autor: Usuario )
Entidade Usuario:
package br.com.virandoprogramador.model import javax.persistence.* @Entity data class Usuario ( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, val nome: String, val email: String )
Classes de Persistência de Dados (JPA Repository)
Agora precisamos criar 3 classes para a persistência de dados com Spring Data JPA.
Acima, anexamos um link para um artigo bem completo sobre o assunto, mais para dar continuidade, vamos criar as 3 classes de repository do projeto: CursoRepository, TopicoRepository e UsuarioRepository.
Package: Repository:
CursoRepository:
package br.com.virandoprogramador.repository import br.com.virandoprogramador.model.Curso import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository interface CursoRepository: JpaRepository<Curso, Long> { }
TopicoRepository:
package br.com.virandoprogramador.repository import br.com.virandoprogramador.model.Topico import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository interface TopicoRepository: JpaRepository<Topico, Long>{ }
UsuarioRepository:
package br.com.virandoprogramador.repository import br.com.virandoprogramador.model.Usuario import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository interface UsuarioRepository: JpaRepository<Usuario, Long> { }
Classes de DTO
Agora, vamos criar nossas classes DTO, para proteger nossas entidades, utilizamos DTOs ou VOs para ter uma flexibilidade maior na exibição dados em uma Api por exemplo, protegendo alguns dados que não precisam ser exibidos em um resultado.
Para iniciar, crie um pacote chamado dto, para separar as classes das entidades. as classes que vamos criar são as seguintes: AtualizacaoTopicoForm, ErrorView, TopicoDTOForm, TopicoView.
Package: Dto.
AtualizacaoTopicoForm:
package br.com.virandoprogramador.dto import javax.validation.constraints.NotEmpty import javax.validation.constraints.NotNull import javax.validation.constraints.Size data class AtualizacaoTopicoForm ( @field:NotNull(message = "ID deve ser Informado!") val id: Long, @field:NotEmpty(message = "Mensagem deve ser Informado!") @field:Size(min = 5, max = 100, message = "Mensagem deve conter entre 5 e 100 Caracteres!") val mensagem: String, @field:NotEmpty(message = "Titulo deve ser Informado!") @field:Size(min = 5, max = 100, message = "Titulo deve conter entre 5 e 100 Caracteres!") val titulo: String, )
ErrorView:
package br.com.virandoprogramador.dto import java.time.LocalDateTime data class ErrorView ( val timestamp: LocalDateTime = LocalDateTime.now(), val status: Int, val erro: String, val message: String?, val path: String )
TopicoDTOForm:
package br.com.virandoprogramador.dto import javax.validation.constraints.NotEmpty import javax.validation.constraints.NotNull import javax.validation.constraints.Size data class TopicoDTOForm ( @field:NotEmpty(message = "Titulo deve ser Informado!") @field:Size(min = 5, max = 100, message = "Titulo deve conter de 5 a 100 Caracteres!") val titulo: String, @field:NotEmpty(message = "Mensagem deve ser Informado!") val mensagem: String, @field:NotNull(message = "ID Curso deve ser Informado!") val idCurso: Long, @field:NotNull(message = "ID Autor deve ser Informado!") val idAutor: Long )
TopicoView:
package br.com.virandoprogramador.dto import br.com.virandoprogramador.model.StatusTopico import java.time.LocalDateTime data class TopicoView ( val id: Long?, val titulo: String, val mensagem: String, val status: StatusTopico, val dataCriacao: LocalDateTime )
Classes complementares – Mapper e Exception
Para dar continuidade, precisamos criar algumas classes complementares em nosso projeto, como Mappers e Exception, para nos ajudar a tratar as exceções e erros.
As classes Mappers vão servir para realizar a conversão de dados para DTO.
Package: Mapper.
Mapper:
package br.com.virandoprogramador.mapper interface Mapper<T, U> { fun map(t: T) :U }
TopicoFormMapper:
package br.com.virandoprogramador.mapper import br.com.virandoprogramador.dto.TopicoDTOForm import br.com.virandoprogramador.model.Topico import br.com.virandoprogramador.service.CursoService import br.com.virandoprogramador.service.UsuarioService import org.springframework.stereotype.Component @Component class TopicoFormMapper(private val cursoService: CursoService, private val usuarioService: UsuarioService): Mapper<TopicoDTOForm, Topico> { override fun map(t: TopicoDTOForm): Topico { return Topico( titulo = t.titulo, mensagem = t.mensagem, curso = cursoService.buscarPorId(t.idCurso), autor = usuarioService.buscarPorId(t.idAutor) ) } }
TopicoViewMapper:
package br.com.virandoprogramador.mapper import br.com.virandoprogramador.dto.TopicoView import br.com.virandoprogramador.model.Topico import org.springframework.stereotype.Component @Component class TopicoViewMapper: Mapper<Topico, TopicoView> { override fun map(t: Topico): TopicoView { return TopicoView( id = t.id, titulo = t.titulo, mensagem = t.mensagem, dataCriacao = t.dataCriacao, status = t.status ) } }
Package: Exception.
ExceptionHandler:
package br.com.virandoprogramador.exception import br.com.virandoprogramador.dto.ErrorView import org.springframework.http.HttpStatus import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.ResponseStatus import javax.servlet.http.HttpServletRequest @RestControllerAdvice class ExceptionHandler { @ExceptionHandler(NotFoundException::class) @ResponseStatus(HttpStatus.NOT_FOUND) fun handleNotFound(exception: NotFoundException, request: HttpServletRequest): ErrorView { return ErrorView( status = HttpStatus.NOT_FOUND.value(), erro = HttpStatus.NOT_FOUND.name, message = exception.message, path = request.servletPath ) } @ExceptionHandler(Exception::class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) fun handleServerError(exception: Exception, request: HttpServletRequest): ErrorView { return ErrorView( status = HttpStatus.INTERNAL_SERVER_ERROR.value(), erro = HttpStatus.INTERNAL_SERVER_ERROR.name, message = exception.message, path = request.servletPath ) } @ExceptionHandler(MethodArgumentNotValidException::class) @ResponseStatus(HttpStatus.BAD_REQUEST) fun handleBadRequest(exception: MethodArgumentNotValidException, request: HttpServletRequest): ErrorView { val errorMessage = HashMap<String, String?>() exception.bindingResult.fieldErrors.forEach{e -> errorMessage.put(e.field, e.defaultMessage)} return ErrorView( status = HttpStatus.BAD_REQUEST.value(), erro = HttpStatus.BAD_REQUEST.name, message = errorMessage.toString(), path = request.servletPath ) } }
NotFoundException:
package br.com.virandoprogramador.exception import java.lang.RuntimeException class NotFoundException(message: String?): RuntimeException(message) { }
Classes da lógica de negócios (Service)
Agora precisamos criar nossas classes para a lógica de negócio da nossa aplicação, antes mesmo de criarmos nossos controllers.
As classes Services, ficaram responsáveis por toda a lógica do nosso código, se conectando com camadas mais a baixo como Mappers e Repository.
Abaixo, vamos criar 3 classes Services: CursoService, TopicoService, UsuarioService.
Package: Service.
CursoService:
package br.com.virandoprogramador.service import br.com.virandoprogramador.model.Curso import br.com.virandoprogramador.repository.CursoRepository import org.springframework.stereotype.Service @Service class CursoService(private val repository: CursoRepository) { fun buscarPorId(id: Long): Curso{ return repository.getOne(id) } }
TopicoService:
package br.com.virandoprogramador.service import br.com.virandoprogramador.dto.AtualizacaoTopicoForm import br.com.virandoprogramador.dto.TopicoDTOForm import br.com.virandoprogramador.dto.TopicoView import br.com.virandoprogramador.exception.NotFoundException import br.com.virandoprogramador.mapper.TopicoFormMapper import br.com.virandoprogramador.mapper.TopicoViewMapper import br.com.virandoprogramador.repository.TopicoRepository import org.springframework.stereotype.Service import java.util.stream.Collectors @Service class TopicoService(private val repository: TopicoRepository, private val topicoViewMapper: TopicoViewMapper, private val topicoFormMapper: TopicoFormMapper, private val notFoundMessage: String = "Topico não Encontrado!" ) { fun listar(): List<TopicoView> { return repository.findAll().stream().map { t -> topicoViewMapper.map(t)}.collect(Collectors.toList()) } fun buscarPorId(id: Long): TopicoView { val topico = repository.findById(id).orElseThrow{NotFoundException(notFoundMessage)} return topicoViewMapper.map(topico) } fun atualizar(form: AtualizacaoTopicoForm): TopicoView{ val topico = repository.findById(form.id).orElseThrow{NotFoundException(notFoundMessage)} topico.titulo = form.titulo topico.mensagem = form.mensagem return topicoViewMapper.map(topico) } fun cadastrar(form: TopicoDTOForm): TopicoView{ val topico = topicoFormMapper.map(form) repository.save(topico) return topicoViewMapper.map(topico) } fun delete(id: Long) { repository.deleteById(id) } }
UsuarioService:
package br.com.virandoprogramador.service import br.com.virandoprogramador.model.Usuario import br.com.virandoprogramador.repository.UsuarioRepository import org.springframework.stereotype.Service @Service class UsuarioService(private val repository: UsuarioRepository) { fun buscarPorId(id: Long): Usuario { return repository.getOne(id) } }
Classes de Controller da Api Rest
Agora, em ultima etapa de código, precisamos criar uma classe de controller, para criar nossos endpoints.
Abaixo vamos criar 5 endpoints utilizando Spring Boot e Kotlin.
Package: Controller.
package br.com.virandoprogramador.controller import br.com.virandoprogramador.dto.AtualizacaoTopicoForm import br.com.virandoprogramador.dto.TopicoDTOForm import br.com.virandoprogramador.dto.TopicoView import br.com.virandoprogramador.service.TopicoService import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import org.springframework.web.util.UriComponentsBuilder import javax.validation.Valid @RestController @RequestMapping("/topico") class TopicoController (private val service: TopicoService){ @GetMapping("/listar") fun listar(): List<TopicoView>{ return service.listar() } @GetMapping("/pesquisar/{id}") fun buscarPorId(@PathVariable id: Long): TopicoView{ return service.buscarPorId(id) } @PutMapping("/atualizar") fun atualizar(@RequestBody @Valid form: AtualizacaoTopicoForm):ResponseEntity<TopicoView>{ val topicoView = service.atualizar(form) return ResponseEntity.ok(topicoView) } @PostMapping("/salvar") fun cadastrar(@RequestBody @Valid form: TopicoDTOForm, uriBuilder: UriComponentsBuilder): ResponseEntity<TopicoView>{ val topicoView = service.cadastrar(form) val uri = uriBuilder.path("/topico/pesquisar/${topicoView.id}").build().toUri() return ResponseEntity.created(uri).body(topicoView) } @DeleteMapping("/delete/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) fun deletar(@PathVariable id: Long){ service.delete(id) } }
Ufa, terminamos! essa foi a nossa ultima classe de configuração, nela criamos 5 endpoints onde você já pode testar em um front-end ou no Postman.
A classe TopicoController vem com os seguintes endpoints:
- listar: retorna uma lista de TopicoView
- buscarPorId: pesquisa por um Id uma entidade de TopicoView
- atualizar: atualiza algum da entidade TopicoView
- cadastrar: cria no banco de dados um registro de Topico
- deletar: deleta por Id
Benefícios da Combinação Kotlin e Spring Boot
Interoperabilidade:
Kotlin é totalmente interoperável com o Java, permitindo que desenvolvedores utilizem bibliotecas Java existentes, incluindo o ecossistema do Spring Boot, sem problemas.
Isso simplifica a migração de projetos Java para Kotlin e oferece flexibilidade para aproveitar as vantagens de ambas as linguagens.
Produtividade Aprimorada:
O Spring Boot, com seu modelo de programação baseado em convenções e configuração automática, complementa perfeitamente a filosofia de Kotlin de reduzir a verbosidade do código.
Isso permite que os desenvolvedores se concentrem mais na lógica de negócios e menos na configuração do framework.
Ecossistema Abundante:
Tanto Kotlin quanto Spring Boot possuem ecossistemas robustos de ferramentas, bibliotecas e recursos de suporte.
Isso significa que os desenvolvedores têm acesso a uma ampla gama de recursos para acelerar o desenvolvimento e resolver problemas comuns de maneira eficiente.
Conclusão sobre Kotlin com Spring Boot
Neste artigo, aprendemos como criar uma API RESTful utilizando Kotlin e Spring Boot. Exploramos a configuração inicial do projeto, a implementação de entidades, repositórios e controladores, bem como a execução da aplicação.
Espero que este guia tenha sido útil para você começar a desenvolver suas próprias APIs com Kotlin e Spring Boot.