Overview
Ktor es un framework web asíncrono basado en Kotlin, diseñado para crear aplicaciones web y API modernas. Ofrece un sólido soporte para la programación asíncrona con corrutinas de Kotlin.
En este tutorial, aprenderá a configurar una API ejecutable utilizando Ktor, el controlador Kotlin y MongoDB Atlas.
Tutorial
Verificar los prerrequisitos
Antes de comenzar este tutorial, asegúrese de tener lo siguiente:
Una cuenta de MongoDB Atlas con un clúster. Para ver las instrucciones, consulte la guía de introducción.
Configurar un proyecto Ktor
Cree un nuevo proyecto de Ktor con el Generador de Proyectos de Ktor. Asigne un nombre al artefacto del proyecto. com.mongodb.fitness-tracker utilizando el campo de entrada de texto. Haga clic en el Configure botón y especifique las siguientes opciones:
Sistema de compilación: Gradle Kotlin
Motor: Tomcat
Configuración: Archivo HOCON
Usando el menú Plugins en la página Nuevo proyecto de Ktor, agregue los siguientes complementos a su proyecto:
Negociación de contenido: Negocia los tipos de medios entre el cliente y el servidor
GSON: Proporciona soporte de serialización y deserialización
Enrutamiento: gestiona las solicitudes entrantes
Swagger: genera documentación de API
Haga clic en el botón Generate Project para descargar un archivo ZIP que contiene su nuevo proyecto Ktor.
Descomprima el archivo descargado y abra el archivo build.gradle.kts en el directorio raíz del proyecto. Agregue los siguientes complementos al bloque plugins:
plugins { kotlin("jvm") version "1.9.22" id("io.ktor.plugin") version "2.3.7" id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22" }
Agregue las siguientes dependencias al bloque dependencies en el mismo archivo:
dependencies { implementation("io.ktor:ktor-server-core-jvm") implementation("io.ktor:ktor-server-swagger-jvm") implementation("io.ktor:ktor-server-content-negotiation-jvm") implementation("io.ktor:ktor-serialization-gson-jvm") implementation("io.ktor:ktor-server-tomcat-jvm") implementation("ch.qos.logback:logback-classic:$logback_version") implementation("io.ktor:ktor-server-config-yaml:2.3.8") // MongoDB implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.6.1") // Koin dependency injection implementation("io.insert-koin:koin-ktor:3.5.3") implementation("io.insert-koin:koin-logger-slf4j:3.5.3") // Client implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-cio:$ktor_version") }
Implementar operaciones CRUD
Cree los siguientes tres paquetes en el directorio src/main/kotlin:
applicationdomaininfrastructure
Dentro del paquete domain, crea un subpaquete llamado entity y, a continuación, crea un archivo dentro de entity llamado Fitness.kt. Copia el siguiente código en el archivo:
package com.mongodb.domain.entity import com.mongodb.application.response.FitnessResponse import org.bson.codecs.pojo.annotations.BsonId import org.bson.types.ObjectId data class Fitness( val id: ObjectId, val exerciseType: String, val notes: String, val details: FitnessDetails ){ fun toResponse() = FitnessResponse( id = id.toString(), exerciseType = exerciseType, notes = notes, details = details ) } data class FitnessDetails( val durationMinutes: Int, val distance: Double, val caloriesBurned: Int )
El método toResponse() del código anterior genera un error porque aún no se ha creado la clase FitnessResponse. Cree los paquetes request y response dentro del paquete application/request y cree dos archivos llamados FitnessRequest.kt y FitnessResponse.kt en el paquete correspondiente.
Copie el siguiente código en el archivo FitnessRequest.kt:
package com.mongodb.application.response import com.mongodb.domain.entity.FitnessDetails data class FitnessResponse( val id: String, val exerciseType: String, val notes: String, val details: FitnessDetails )
Copie el siguiente código en el archivo FitnessResponse.kt:
package com.mongodb.application.response import com.mongodb.domain.entity.FitnessDetails data class FitnessResponse( val id: String, val exerciseType: String, val notes: String, val details: FitnessDetails )
Cree un paquete ports dentro del paquete domain y cree un archivo llamado FitnessRepository.kt dentro del paquete ports. Este archivo representa la interfaz que se comunicará con la base de datos. Copie el siguiente código en el archivo FitnessRepository.kt:
package com.mongodb.domain.ports import com.mongodb.domain.entity.Fitness import org.bson.BsonValue import org.bson.types.ObjectId interface FitnessRepository { suspend fun insertOne(fitness: Fitness): BsonValue? suspend fun deleteById(objectId: ObjectId): Long suspend fun findById(objectId: ObjectId): Fitness? suspend fun updateOne(objectId: ObjectId, fitness: Fitness): Long }
Cree un paquete repository dentro del paquete infrastructure y un archivo llamado FitnessRepositoryImpl.kt dentro del paquete repository. Este archivo implementa los métodos de la interfaz. Copie el siguiente código en el archivo FitnessRepositoryImpl.kt:
package com.mongodb.infrastructure.repository import com.mongodb.MongoException import com.mongodb.client.model.Filters import com.mongodb.client.model.UpdateOptions import com.mongodb.client.model.Updates import com.mongodb.domain.entity.Fitness import com.mongodb.domain.ports.FitnessRepository import com.mongodb.kotlin.client.coroutine.MongoDatabase import kotlinx.coroutines.flow.firstOrNull import org.bson.BsonValue import org.bson.types.ObjectId class FitnessRepositoryImpl( private val mongoDatabase: MongoDatabase ) : FitnessRepository { companion object { const val FITNESS_COLLECTION = "fitness" } override suspend fun insertOne(fitness: Fitness): BsonValue? { try { val result = mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION).insertOne( fitness ) return result.insertedId } catch (e: MongoException) { System.err.println("Unable to insert due to an error: $e") } return null } override suspend fun deleteById(objectId: ObjectId): Long { try { val result = mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION).deleteOne(Filters.eq("_id", objectId)) return result.deletedCount } catch (e: MongoException) { System.err.println("Unable to delete due to an error: $e") } return 0 } override suspend fun findById(objectId: ObjectId): Fitness? = mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION).withDocumentClass<Fitness>() .find(Filters.eq("_id", objectId)) .firstOrNull() override suspend fun updateOne(objectId: ObjectId, fitness: Fitness): Long { try { val query = Filters.eq("_id", objectId) val updates = Updates.combine( Updates.set(Fitness::exerciseType.name, fitness.exerciseType), Updates.set(Fitness::notes.name, fitness.notes), Updates.set(Fitness::details.name, fitness.details) ) val options = UpdateOptions().upsert(true) val result = mongoDatabase.getCollection<Fitness>(FITNESS_COLLECTION) .updateOne(query, updates, options) return result.modifiedCount } catch (e: MongoException) { System.err.println("Unable to update due to an error: $e") } return 0 } }
Configurar complementos
Pegue el siguiente código en el archivo Application.kt. Este código realiza las siguientes acciones:
Configura el complemento ContentNegotiation para manejar la serialización y deserialización de JSON mediante el formateador Gson
Permite la inyección de dependencia mediante el uso de Koin
Configura la ruta de la API Swagger, que está disponible en el punto final
/swagger
package com.mongodb import com.mongodb.application.routes.fitnessRoutes import com.mongodb.domain.ports.FitnessRepository import com.mongodb.infrastructure.repository.FitnessRepositoryImpl import com.mongodb.kotlin.client.coroutine.MongoClient import io.ktor.serialization.gson.gson import io.ktor.server.application.Application import io.ktor.server.application.install import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.swagger.swaggerUI import io.ktor.server.routing.routing import io.ktor.server.tomcat.EngineMain import org.koin.dsl.module import org.koin.ktor.plugin.Koin import org.koin.logger.slf4jLogger fun main(args: Array<String>): Unit = EngineMain.main(args) fun Application.module() { install(ContentNegotiation) { gson { } } install(Koin) { slf4jLogger() modules(module { single { MongoClient.create( environment.config.propertyOrNull("ktor.mongo.uri")?.getString() ?: throw RuntimeException("Failed to access MongoDB URI.") ) } single { get<MongoClient>().getDatabase(environment.config.property("ktor.mongo.database").getString()) } }, module { single<FitnessRepository> { FitnessRepositoryImpl(get()) } }) } routing { swaggerUI(path = "swagger-ui", swaggerFile = "openapi/documentation.yaml") { version = "4.15.5" } fitnessRoutes() } }
Abra el archivo application.conf y pegue lo siguiente:
ktor { deployment { port = 8080 } application { modules = [ com.mongodb.ApplicationKt.module ] } mongo { uri = ${?MONGO_URI} database = ${?MONGO_DATABASE} } }
Abra el archivo documentation.yaml y pegue lo siguiente:
openapi: 3.0.0 info: title: Fitness API version: 1.0.0 description: | 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. paths: /fitness: post: summary: Create a new fitness record requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/FitnessRequest' responses: '201': description: Fitness created successfully '400': description: Bad request /fitness/{id}: get: summary: Retrieve fitness record by ID parameters: - name: id in: path required: true schema: type: string responses: '200': description: Successful response content: application/json: example: {} '404': description: Fitness not found delete: summary: Delete fitness record by ID parameters: - name: id in: path required: true schema: type: string responses: '200': description: Fitness deleted successfully '400': description: Bad request '404': description: Fitness not found patch: summary: Update fitness record by ID parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/FitnessRequest' responses: '200': description: Fitness updated successfully '400': description: Bad request '404': description: Fitness not found components: schemas: Fitness: type: object properties: id: type: string format: uuid exerciseType: type: string notes: type: string details: $ref: '#/components/schemas/FitnessDetails' required: - id - notes - details FitnessDetails: type: object properties: durationMinutes: type: integer format: int32 distance: type: number format: double caloriesBurned: type: integer format: int32 required: - durationMinutes - distance - caloriesBurned FitnessRequest: type: object properties: exerciseType: type: string notes: type: string details: $ref: '#/components/schemas/FitnessDetails' required: - exerciseType - notes - details
Este archivo define qué métodos proporciona nuestra API a través del punto final /swagger-ui.
Implementar puntos finales de API
Este paso implementa los siguientes puntos finales de API:
GET /fitness/{id}: recupera una entrada de fitness por su ID
POST /fitness: Crea una nueva entrada de fitness
PATCH /fitness/{id}: actualiza la entrada de aptitud especificada
ELIMINAR /fitness/{id}: Elimina la entrada de fitness especificada
Cree un paquete routes dentro del paquete application y cree un archivo llamado FitnessRoutes.kt dentro del paquete routes. Copie el siguiente código en el archivo FitnessRoutes.kt:
package com.mongodb.application.routes import com.mongodb.application.request.FitnessRequest import com.mongodb.application.request.toDomain import com.mongodb.domain.ports.FitnessRepository import io.ktor.http.HttpStatusCode import io.ktor.server.application.call import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.response.respondText import io.ktor.server.routing.route import io.ktor.server.routing.Route import io.ktor.server.routing.post import io.ktor.server.routing.delete import io.ktor.server.routing.get import io.ktor.server.routing.patch import org.bson.types.ObjectId import org.koin.ktor.ext.inject fun Route.fitnessRoutes() { val repository by inject<FitnessRepository>() route("/fitness") { post { val fitness = call.receive<FitnessRequest>() val insertedId = repository.insertOne(fitness.toDomain()) call.respond(HttpStatusCode.Created, "Created fitness with id $insertedId") } delete("/{id?}") { val id = call.parameters["id"] ?: return@delete call.respondText( text = "Missing fitness id", status = HttpStatusCode.BadRequest ) val delete: Long = repository.deleteById(ObjectId(id)) if (delete == 1L) { return@delete call.respondText("Fitness Deleted successfully", status = HttpStatusCode.OK) } return@delete call.respondText("Fitness not found", status = HttpStatusCode.NotFound) } get("/{id?}") { val id = call.parameters["id"] if (id.isNullOrEmpty()) { return@get call.respondText( text = "Missing id", status = HttpStatusCode.BadRequest ) } repository.findById(ObjectId(id))?.let { call.respond(it.toResponse()) } ?: call.respondText("No records found for id $id") } patch("/{id?}") { val id = call.parameters["id"] ?: return@patch call.respondText( text = "Missing fitness id", status = HttpStatusCode.BadRequest ) val updated = repository.updateOne(ObjectId(id), call.receive()) call.respondText( text = if (updated == 1L) "Fitness updated successfully" else "Fitness not found", status = if (updated == 1L) HttpStatusCode.OK else HttpStatusCode.NotFound ) } } }
El código anterior define los puntos finales de la API y sus controladores.
Antes de ejecutar la aplicación, elimine la carpeta plugins que contiene los archivos HTTP.kt, Routing.kt y Serialization.kt, porque ya los hemos incluido en el archivo Application.kt.
Ejecutar la aplicación
Recupere la URI de conexión de su clúster de MongoDB Atlas. Para obtener más información sobre cómo hacerlo, consulte Sección URI de conexión de la guía Conectarse a MongoDB.
Abra el archivo application.conf y agregue los siguientes valores de configuración:
-DMONGO_URI= <your connection URI> -DMONGO_DATABASE= <your database name>
Acceda al archivo Application.kt en IntelliJ y haga clic en el botón Run. Ahora puede acceder a la API accediendo a http://localhost:8080/swagger-ui/ en un navegador web.
Información Adicional
Para obtener más información sobre Ktor, consulte la documentación de Ktor.
Para ver el código fuente de este tutorial, consulte el repositorio Kotlin Ktor MongoDB Vector Search en GitHub.