Dominando o Kotlin: criando uma API com Ktor e MongoDB Atlas
Avalie esse Tutorial
Asimplicidade do Kotlin, a interoperabilidade Java e a estrutura fácil de usar do Ktor, combinadas com o banco de dados em nuvem flexível do MongoDB Atlas, fornecem uma pilha robusta para o desenvolvimentode software moderno.
Juntos, demonstraremos e configuraremos o projeto Ktor, implementaremos operações CRUD, definiremos pontos de conexão de rota de API e executaremos o aplicativo. Ao final, você terá uma sólida compreensão dos recursos do Kotlin no desenvolvimento de API e das ferramentas necessárias para ter sucesso no desenvolvimento de software moderno.
Como você pode ver acima, nosso aplicativo será capaz de executar as operações descritas na imagem. Para fazer isso, utilizaremos um modelo de dados estruturado semelhante ao exemplo fornecido:
1 fitness { 2 _id: objectId, 3 exerciseType: String, 4 notes: String, 5 fitnessDetails: { 6 durationMinutes: Int, 7 distance: Double, 8 caloriesBurned: Int 9 } 10 }
O Ktor é uma estrutura web assíncrona e baseada em Kotlin, projetada para criar aplicativos web e APIs modernos. Desenvolvido pela JetBrains, a mesma equipe por trás do Kotlin, o Ktor simplifica o processo de criação de aplicativos da web e, ao mesmo tempo, oferece suporte robusto para programação assíncrona usando corrotinas do Kotlin.
Conforme representado nas imagens, precisamos configurar parâmetros como nome do projeto, arquivo de configuração etc.
Na seção subsequente, especificamos os seguintes plug-ins:
- Negociação de conteúdo: facilita a negociação de tipos de mídia entre o cliente e o servidor
- GSON: oferece recursos de serialização e desserialização
- Roteamento: gerencia solicitações recebidas em um aplicativo de servidor
- Swagger: simplifica a documentação e o desenvolvimento da API
Depois que todas as configurações estiverem em vigor, basta clicar em "Generate Project " para continuar com a criação de nosso projeto.
Após importar o projeto, vamos abrir o arquivo
build.gradle.kts
para incorporar dependências adicionais. Especificamente, adicionaremos dependências para o driver Kotlin do lado do servidor MongoDB e Koin para dependência de injeção.1 dependencies { 2 implementation("io.ktor:ktor-server-core-jvm") 3 implementation("io.ktor:ktor-server-swagger-jvm") 4 implementation("io.ktor:ktor-server-content-negotiation-jvm") 5 implementation("io.ktor:ktor-serialization-gson-jvm") 6 implementation("io.ktor:ktor-server-tomcat-jvm") 7 implementation("ch.qos.logback:logback-classic:$logback_version") 8 9 //MongoDB 10 implementation("org.mongodb:mongodb-driver-kotlin-coroutine:4.10.1") 11 12 //Koin Dependency Injection 13 implementation("io.insert-koin:koin-ktor:3.5.3") 14 implementation("io.insert-koin:koin-logger-slf4j:3.5.3") 15 }
Antes de criar nosso projeto, vamos organiza-lo em vários pacotes. Para isso, criaremos três pacotes:
application
, domain
e infrastructure
como mostrado na imagem abaixo:Agora, vamos criar um pacote chamado
entity
dentro do domínio e incluir um arquivo chamado Fitness.kt:domain/entity/Fitness.kt
1 package com.mongodb.domain.entity 2 import com.mongodb.application.response.FitnessResponse 3 import org.bson.codecs.pojo.annotations.BsonId 4 import org.bson.types.ObjectId 5 6 data class Fitness( 7 8 val id: ObjectId, 9 val exerciseType: String, 10 val notes: String, 11 val details: FitnessDetails 12 ){ 13 fun toResponse() = FitnessResponse( 14 id = id.toString(), 15 exerciseType = exerciseType, 16 notes = notes, 17 details = details 18 ) 19 } 20 21 data class FitnessDetails( 22 val durationMinutes: Int, 23 val distance: Double, 24 val caloriesBurned: Int 25 )
Como você pode observar, nossa classe tem um erro no
toResponse()
método porque ainda não criamos a FitnessResponse
classe . Para corrigir esse problema, precisamos criar a FitnessResponse
classe . Vamos aproveitar esta oportunidade para criar FitnessResponse
e FitnessRequest
. Essas classes manipularão os dados trocados em solicitações HTTP relacionadas à entidade Fitness. Dentro do pacote do aplicativo,request
response
crie os pacotes e . Inclua as FitnessRequest
FitnessResponse
classes e em cada uma, respectivamente.application/request/FitnessRequest.kt:
1 package com.mongodb.application.request 2 3 import com.mongodb.domain.entity.Fitness 4 import com.mongodb.domain.entity.FitnessDetails 5 import org.bson.types.ObjectId 6 7 data class FitnessRequest( 8 val exerciseType: String, 9 val notes: String, 10 val details: FitnessDetails 11 ) 12 fun FitnessRequest.toDomain(): Fitness { 13 return Fitness( 14 id = ObjectId(), 15 exerciseType = exerciseType, 16 notes = notes, 17 details = details 18 ) 19 }
application/response/FitnessResponse.kt:
1 package com.mongodb.application.response 2 3 4 import com.mongodb.domain.entity.FitnessDetails 5 6 7 data class FitnessResponse( 8 val id: String, 9 val exerciseType: String, 10 val notes: String, 11 val details: FitnessDetails 12 )
Se tudo estiver correto, nossa estrutura ficará semelhante à imagem abaixo:
Agora, é hora de criar nossa interface que se comunicará com nosso banco de dados. Para fazer isso, dentro do pacote
domain
, criaremos outro pacote chamado ports
e, posteriormente, a interfaceFitnessRepository
.domain/ports/FitnessRepository:
1 package com.mongodb.domain.ports 2 3 import com.mongodb.domain.entity.Fitness 4 import org.bson.BsonValue 5 import org.bson.types.ObjectId 6 7 interface FitnessRepository { 8 suspend fun insertOne(fitness: Fitness): BsonValue? 9 suspend fun deleteById(objectId: ObjectId): Long 10 suspend fun findById(objectId: ObjectId): Fitness? 11 suspend fun updateOne(objectId: ObjectId, fitness: Fitness): Long 12 }
Ideal! Agora, precisamos implementar os métodos de nossa interface. Acessaremos o pacote
infrastructure
e criaremos um pacoterepository
dentro dele.Em seguida, crie uma classeFitnessRepositoryImpl
e implemente os métodos conforme mostrado no código abaixo:infrastructure/repository/FitnessRepositoryImpl
1 package com.mongodb.infrastructure.repository 2 3 import com.mongodb.MongoException 4 import com.mongodb.client.model.Filters 5 import com.mongodb.client.model.UpdateOptions 6 import com.mongodb.client.model.Updates 7 import com.mongodb.domain.entity.Fitness 8 import com.mongodb.domain.ports.FitnessRepository 9 import com.mongodb.kotlin.client.coroutine.MongoDatabase 10 import kotlinx.coroutines.flow.firstOrNull 11 import org.bson.BsonValue 12 import org.bson.types.ObjectId 13 14 class FitnessRepositoryImpl( 15 private val mongoDatabase: MongoDatabase 16 ) : FitnessRepository { 17 18 companion object { 19 const val FITNESS_COLLECTION = "fitness" 20 } 21 22 override suspend fun insertOne(fitness: Fitness): BsonValue? { 23 try { 24 val result = mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION).insertOne( 25 fitness 26 ) 27 return result.insertedId 28 } catch (e: MongoException) { 29 System.err.println("Unable to insert due to an error: $e") 30 } 31 return null 32 } 33 34 override suspend fun deleteById(objectId: ObjectId): Long { 35 try { 36 val result = mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION).deleteOne(Filters.eq("_id", objectId)) 37 return result.deletedCount 38 } catch (e: MongoException) { 39 System.err.println("Unable to delete due to an error: $e") 40 } 41 return 0 42 } 43 44 override suspend fun findById(objectId: ObjectId): Fitness? = 45 mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION).withDocumentClass<Fitness>() 46 .find(Filters.eq("_id", objectId)) 47 .firstOrNull() 48 49 override suspend fun updateOne(objectId: ObjectId, fitness: Fitness): Long { 50 try { 51 val query = Filters.eq("_id", objectId) 52 val updates = Updates.combine( 53 Updates.set(Fitness::exerciseType.name, fitness.exerciseType), 54 Updates.set(Fitness::notes.name, fitness.notes), 55 Updates.set(Fitness::details.name, fitness.details) 56 ) 57 val options = UpdateOptions().upsert(true) 58 val result = 59 mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION) 60 .updateOne(query, updates, options) 61 62 return result.modifiedCount 63 } catch (e: MongoException) { 64 System.err.println("Unable to update due to an error: $e") 65 } 66 return 0 67 } 68 }
Conforme observado, a classe implementa a interface
FitnessRepository
e inclui um objeto complementar. Ele recebe uma instânciaMongoDatabase
como parâmetro do construtor. O objeto complementar define uma constante chamada FITNESS_COLLECTION
, com um valor de "fitness, " indicando o nome da collection do MongoDB em que operações como inserir, localizar, excluir e atualizar são realizadas em dados relacionados à aptidão.Se tudo estiver correto, nossa estrutura será a seguinte:
Para criar nossos endpoints, é fundamentalcompreender oroteamento. No Ktor, o roteamento determina como o servidor lida com as solicitações de entrada para URLs específicos, permitindo que os desenvolvedores definam ações ou respostas para cada endpoint. Antes de prosseguir com o processo de criação, vamos revisar brevemente os endpoints que estaremos construindo:
- GET /Fitness/{id}: utilize este endpoint para obter informações detalhadas sobre uma atividade de preparação física específica com base em seu identificador exclusivo (ID).
- POST /fitness: com esse endpoint, você pode adicionar facilmente novas atividades de condicionamento físico ao seu monitor, garantindo que todos os dados do seu treinoestejam atualizados.
- PATCH /Fitness/{ID}: precisa atualizar ou modificar uma atividade de preparação física existente? Esse endpoint permite que você faça alterações direcionadas a atividades específicas identificadas por seu ID exclusivo.
- DELETE /fitness/{id}: por fim, esse endpoint permite que você remova atividades de condicionamento físico indesejadas ou desatualizadas do seu rastreador, mantendo um registro limpo e preciso de sua jornada de condicionamento físico.
Agora que sabemos os métodos que implementaremos, vamos criar a classe que fará esse trabalho. Dentro do pacote
application
, vamos criar um novo chamado routes
e o arquivo abaixo com o nome FitnessRoute.kt
com o conteúdo abaixo:application/routes/FitnessRoutes.kt
1 package com.mongodb.application.routes 2 3 import com.mongodb.application.request.FitnessRequest 4 import com.mongodb.application.request.toDomain 5 import com.mongodb.domain.ports.FitnessRepository 6 import io.ktor.http.HttpStatusCode 7 import io.ktor.server.application.call 8 import io.ktor.server.request.receive 9 import io.ktor.server.response.respond 10 import io.ktor.server.response.respondText 11 import io.ktor.server.routing.route 12 import io.ktor.server.routing.Route 13 import io.ktor.server.routing.post 14 import io.ktor.server.routing.delete 15 import io.ktor.server.routing.get 16 import io.ktor.server.routing.patch 17 import org.bson.types.ObjectId 18 import org.koin.ktor.ext.inject 19 20 fun Route.fitnessRoutes() { 21 val repository by inject<FitnessRepository>() 22 route("/fitness") { 23 post { 24 val fitness = call.receive<FitnessRequest>() 25 val insertedId = repository.insertOne(fitness.toDomain()) 26 call.respond(HttpStatusCode.Created, "Created fitness with id $insertedId") 27 } 28 29 delete("/{id?}") { 30 val id = call.parameters["id"] ?: return@delete call.respondText( 31 text = "Missing fitness id", 32 status = HttpStatusCode.BadRequest 33 ) 34 val delete: Long = repository.deleteById(ObjectId(id)) 35 if (delete == 1L) { 36 return@delete call.respondText("Fitness Deleted successfully", status = HttpStatusCode.OK) 37 } 38 return@delete call.respondText("Fitness not found", status = HttpStatusCode.NotFound) 39 } 40 41 get("/{id?}") { 42 val id = call.parameters["id"] 43 if (id.isNullOrEmpty()) { 44 return@get call.respondText( 45 text = "Missing id", 46 status = HttpStatusCode.BadRequest 47 ) 48 } 49 repository.findById(ObjectId(id))?.let { 50 call.respond(it.toResponse()) 51 } ?: call.respondText("No records found for id $id") 52 } 53 54 patch("/{id?}") { 55 val id = call.parameters["id"] ?: return@patch call.respondText( 56 text = "Missing fitness id", 57 status = HttpStatusCode.BadRequest 58 ) 59 val updated = repository.updateOne(ObjectId(id), call.receive()) 60 call.respondText( 61 text = if (updated == 1L) "Fitness updated successfully" else "Fitness not found", 62 status = if (updated == 1L) HttpStatusCode.OK else HttpStatusCode.NotFound 63 ) 64 } 65 } 66 }
Como você pode observar, configuramos chamadas de método e utilizamos
call
para definir ações nos manipuladores de rotas. O Ktor permite gerenciar solicitações recebidas e enviar respostas diretamente dentro desses manipuladores, oferecendo flexibilidade no tratamento de diferentes cenários de solicitação. Além disso, estamos utilizando o Koin para injeção de dependência no FitnessRepository
repositório .Agora, antes de executarmos o aplicativo, precisamos instalar a dependência Koin e incorporá-la como um módulo.
Os módulos no Ktor são usados para organizar e encapsular a funcionalidade em seu aplicativo. Eles permitem agrupar rotas, dependências e configurações relacionadas para facilitar o gerenciamento e a manutenção.
Para isso, abra a classe
Application.kt
e substitua todo o seu código pelo código abaixo:com.mongodb.Application.kt
1 package com.mongodb 2 3 import com.mongodb.application.routes.fitnessRoutes 4 import com.mongodb.domain.ports.FitnessRepository 5 import com.mongodb.infrastructure.repository.FitnessRepositoryImpl 6 import com.mongodb.kotlin.client.coroutine.MongoClient 7 import io.ktor.serialization.gson.gson 8 import io.ktor.server.application.Application 9 import io.ktor.server.application.install 10 import io.ktor.server.plugins.contentnegotiation.ContentNegotiation 11 import io.ktor.server.plugins.swagger.swaggerUI 12 import io.ktor.server.routing.routing 13 import io.ktor.server.tomcat.EngineMain 14 import org.koin.dsl.module 15 import org.koin.ktor.plugin.Koin 16 import org.koin.logger.slf4jLogger 17 18 fun main(args: Array<String>): Unit = EngineMain.main(args) 19 20 fun Application.module() { 21 install(ContentNegotiation) { 22 gson { 23 } 24 } 25 install(Koin) { 26 slf4jLogger() 27 modules(module { 28 single { MongoClient.create( 29 environment.config.propertyOrNull("ktor.mongo.uri")?.getString() ?: throw RuntimeException("Failed to access MongoDB URI.") 30 ) } 31 single { get<MongoClient>().getDatabase(environment.config.property("ktor.mongo.database").getString()) } 32 }, module { 33 single<FitnessRepository> { FitnessRepositoryImpl(get()) } 34 }) 35 } 36 routing { 37 swaggerUI(path = "swagger-ui", swaggerFile = "openapi/documentation.yaml") { 38 version = "4.15.5" 39 } 40 fitnessRoutes() 41 } 42 }
Vamos dividir o que esse código está fazendo em três seções principais para facilitar a compreensão:
- Configuração do ContentNegotiation: estamos configurando o ContentNegotiation para lidar com a serialização e a desserialização JSON. Especificamente, estamos usando o Gson como formatador JSON padrão, garantindo uma comunicação perfeita entre nosso aplicativo e os clientes.
- Injeção de dependência com Koin**:** Na seção subsequente, estamos integrando Koin para gerenciamento de injeção de dependência. No primeiro módulo, estabelecemos a conexão com o MongoDB, recuperando o URI e o nome do banco de dados do arquivo configuration.conf. Posteriormente, definimos injeção para o FitnessRepository.
- Configuração de roteamento: Durante a fase de roteamento, configuramos a rota da API Swagger, acessível em
/swagger-ui
. Além disso, especificamos nossa rota para o tratamento de operações relacionadas ao Fitness.
Ótimo. Agora precisamos fazer os ajustes finais antes de executar nossa aplicação. Primeiro, abra o arquivo
application.conf
e inclua as seguintes informações:recursos/application.conf
1 ktor { 2 deployment { 3 port = 8080 4 } 5 application { 6 modules = [ com.mongodb.ApplicationKt.module ] 7 } 8 mongo { 9 uri = ${?MONGO_URI} 10 database = ${?MONGO_DATABASE} 11 } 12 }
Aviso: Essas variáveis serão utilizadas no final do artigo quando executarmos a aplicação.
Muito bem. Agora, basta abrir o arquivo
documentation.yaml
e substituí-lo pelo conteúdo abaixo. Neste arquivo, estamos indicando quais métodos nossa API fornecerá quando acessada por meio /swagger-ui
:recursos/openapi/documentation.yaml
1 openapi: 3.0.0 2 info: 3 title: Fitness API 4 version: 1.0.0 5 description: | 6 This Swagger documentation file outlines the API specifications for a Fitness Tracker application built with Ktor and MongoDB. The API allows users to manage fitness records including creating new records, updating and deleting records by ID. The API uses the Fitness and FitnessDetails data classes to structure the fitness-related information. 7 8 paths: 9 /fitness: 10 post: 11 summary: Create a new fitness record 12 requestBody: 13 required: true 14 content: 15 application/json: 16 schema: 17 $ref: '#/components/schemas/FitnessRequest' 18 responses: 19 '201': 20 description: Fitness created successfully 21 '400': 22 description: Bad request 23 /fitness/{id}: 24 get: 25 summary: Retrieve fitness record by ID 26 parameters: 27 - name: id 28 in: path 29 required: true 30 schema: 31 type: string 32 responses: 33 '200': 34 description: Successful response 35 content: 36 application/json: 37 example: {} 38 '404': 39 description: Fitness not found 40 delete: 41 summary: Delete fitness record by ID 42 parameters: 43 - name: id 44 in: path 45 required: true 46 schema: 47 type: string 48 responses: 49 '200': 50 description: Fitness deleted successfully 51 '400': 52 description: Bad request 53 '404': 54 description: Fitness not found 55 patch: 56 summary: Update fitness record by ID 57 parameters: 58 - name: id 59 in: path 60 required: true 61 schema: 62 type: string 63 requestBody: 64 required: true 65 content: 66 application/json: 67 schema: 68 $ref: '#/components/schemas/FitnessRequest' 69 responses: 70 '200': 71 description: Fitness updated successfully 72 '400': 73 description: Bad request 74 '404': 75 description: Fitness not found 76 77 components: 78 schemas: 79 Fitness: 80 type: object 81 properties: 82 id: 83 type: string 84 format: uuid 85 exerciseType: 86 type: string 87 notes: 88 type: string 89 details: 90 $ref: '#/components/schemas/FitnessDetails' 91 required: 92 - id 93 - notes 94 - details 95 96 FitnessDetails: 97 type: object 98 properties: 99 durationMinutes: 100 type: integer 101 format: int32 102 distance: 103 type: number 104 format: double 105 caloriesBurned: 106 type: integer 107 format: int32 108 required: 109 - durationMinutes 110 - distance 111 - caloriesBurned 112 113 FitnessRequest: 114 type: object 115 properties: 116 exerciseType: 117 type: string 118 notes: 119 type: string 120 details: 121 $ref: '#/components/schemas/FitnessDetails' 122 required: 123 - exerciseType 124 - notes 125 - details
O ajuste final é excluir a pasta
plugins
, que contém os arquivosHTTP.kt
, Routing.kt
e Serialization.kt
, pois já os incluímos na classeApplication.kt
. Além disso, exclua a classeApplicationTest
, pois nosso foco aqui não está nos testes. Podemos discutir testes outra hora. Após essas alterações, sua estrutura deverá ficar semelhante a esta:Para executar nosso aplicativo, precisamos de uma connection string do MongoDB Atlas. Se você ainda não tem acesso, crie sua conta.
Depois que sua conta for criada, acesse o menuVisão geral ,depois em Conectar e selecione Kotlin. Depois disso, nossa connection string estará disponível conforme mostrado na imagem abaixo:
Com a connection string em mãos, vamos retornar ao IntelliJ, abra a classe
Application.kt
e clique no botão executar, conforme mostrado na imagem:Nesse estágio, você notará que nosso aplicativo encontrou um erro ao se conectar ao MongoDB Atlas. É aqui que precisamos fornecer o
MONGO_URI
e MONGO_DATABASE
definidos no arquivoapplication.conf
:Para fazer isso, basta editar a configuração e incluir os valores conforme mostrado na imagem abaixo:
1 -DMONGO_URI= Add your connection string 2 -DMONGO_DATABASE= Add your database name
Clique em “Apply” e execute o aplicativo novamente.
Atenção: lembre-se de alterar a connection string para seu nome de usuário, senha e cluster no MongoDB Atlas.
Agora, basta acessar
localhost:8080/swagger-ui
e executar as operações.Abra o método de postagem e insira um objeto para teste:
Podemos visualizar os dados registrados em nosso cluster MongoDB Atlas como mostrado na imagem abaixo:
O serviço de API MongoDB Atlas, Ktor e Kotlin juntos forma uma combinação poderosa para criar aplicativos robustos e escaláveis. O MongoDB Atlas fornece uma solução de banco de dados baseada na nuvem que oferece flexibilidade, escalabilidade e confiabilidade, tornando-o ideal para aplicativos modernos. O Ktor, com sua natureza leve e assíncrona, permite o rápido desenvolvimento de serviços web e APIs em Kotlin, aproveitando a sintaxe concisa e os recursos avançados da linguagem. Quando combinados, o MongoDB Atlas e o Ktor em Kotlin permitem que os desenvolvedores criem APIs de alto desempenho com facilidade, fornecendo integração perfeita com bancos de dados MongoDB e mantendo a simplicidade e a flexibilidade no desenvolvimento de aplicativos. Essa combinação capacita os desenvolvedores a criar aplicativos modernos e orientados por dados que podem ser facilmente dimensionados para atender às necessidades de negócios em evolução.
O exemplo de código-fonte usado nesta série está disponível no Github
Principais comentários nos fóruns
Lucas_Carrijo_FerrariLucas Carrijo Ferrarilast quarter
Muito obrigado pelo artigo. Achei muito mais claro sua explicação de como conectar o Mongo, mais claro que a própria documentação do Ktor explicando como se conecta no Postgresql.
Só um comentário, o Ktor recomenda, além do GSON que você utilizou, o KotlinX para serialização, a implementação é da mesma forma?
Pelo nome já percebi que era brasileiro (tambem fui no Linkedin pra ter certeza haha).