Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
Kotlin
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
Kotlinchevron-right

Mastering Kotlin: Creating an API With Ktor and MongoDB Atlas

Ricardo Mello9 min read • Published Sep 17, 2024 • Updated Sep 17, 2024
Kotlin
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Kotlin's simplicity, Java interoperability, and Ktor's user-friendly framework combined with MongoDB Atlas' flexible cloud database provide a robust stack for modern software development.
Together, we'll demonstrate and set up the Ktor project, implement CRUD operations, define API route endpoints, and run the application. By the end, you'll have a solid understanding of Kotlin's capabilities in API development and the tools needed to succeed in modern software development.

Demonstration

Demo of Kotlin and Ktor Project with MongoDB
As you can see above, our application will be capable of performing the operations depicted in the image. To accomplish this, we will utilize a data model structured similarly to the example provided:
1fitness {
2 _id: objectId,
3 exerciseType: String,
4 notes: String,
5 fitnessDetails: {
6 durationMinutes: Int,
7 distance: Double,
8 caloriesBurned: Int
9 }
10}

Setting up the Ktor project

Ktor is a Kotlin-based, asynchronous web framework designed for building modern web applications and APIs. Developed by JetBrains, the same team behind Kotlin, Ktor simplifies the process of building web applications while offering robust support for asynchronous programming using Kotlin coroutines.
To begin our project setup, we’ll make use of the Ktor project generator.
Ktor Project Generator - Adjusting project settings
As depicted in the images, we need to configure parameters such as the project name, configuration file, etc.
In the subsequent section, we specify the following plugins:
  • Content Negotiation:  facilitates the negotiation of media types between the client and server
  • GSON: offers serialization and deserialization capabilities
  • Routing: manages incoming requests within a server application
  • Swagger: simplifies API documentation and development
Ktor Project Generator - Adding plugins
Once all settings are in place, simply click “Generate Project” to proceed with creating our project.
After importing the project, let’s proceed by opening the build.gradle.kts file to incorporate additional dependencies. Specifically, we'll be adding dependencies for the MongoDB server-side Kotlin driver and Koin for injection dependency.
1dependencies {
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}

Implementing CRUD operations

Before creating our project, let’s organize it into several packages. To achieve this, we’ll create three packages: application, domain, and infrastructure as shown in the image below:
Project structure
Now, let's create a package called entity inside domain and include a file named Fitness.kt:
domain/entity/Fitness.kt
1package com.mongodb.domain.entity
2import com.mongodb.application.response.FitnessResponse
3import org.bson.codecs.pojo.annotations.BsonId
4import org.bson.types.ObjectId
5
6data class Fitness(
7 @BsonId
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
21data class FitnessDetails(
22 val durationMinutes: Int,
23 val distance: Double,
24 val caloriesBurned: Int
25)
As you can observe, our class has an error in the toResponse() method because we haven't created the FitnessResponse class yet. To fix this issue, we need to create the FitnessResponse class. Let's take this opportunity to create both FitnessResponse and FitnessRequest. These classes will handle the data exchanged in HTTP requests related to the Fitness entity. Inside the application package, create request and response packages. Include the FitnessRequest and FitnessResponse classes in each, respectively.
application/request/FitnessRequest.kt:
1package com.mongodb.application.request
2
3import com.mongodb.domain.entity.Fitness
4import com.mongodb.domain.entity.FitnessDetails
5import org.bson.types.ObjectId
6
7data class FitnessRequest(
8 val exerciseType: String,
9 val notes: String,
10 val details: FitnessDetails
11)
12fun FitnessRequest.toDomain(): Fitness {
13 return Fitness(
14 id = ObjectId(),
15 exerciseType = exerciseType,
16 notes = notes,
17 details = details
18 )
19}
application/response/FitnessResponse.kt:
1package com.mongodb.application.response
2
3
4import com.mongodb.domain.entity.FitnessDetails
5
6
7data class FitnessResponse(
8 val id: String,
9 val exerciseType: String,
10 val notes: String,
11 val details: FitnessDetails
12)
If everything is correct, our structure will look similar to the image below:
Project structure
Now, it’s time to create our interface that will communicate with our database. To do this, within the domain package, we will create another package called ports, and subsequently, the FitnessRepository interface.
domain/ports/FitnessRepository:
1package com.mongodb.domain.ports
2
3import com.mongodb.domain.entity.Fitness
4import org.bson.BsonValue
5import org.bson.types.ObjectId
6
7interface 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}
Perfect! Now, we need to implement the methods of our interface. We'll access the infrastructure package and create a repository package inside it. After that, create a class FitnessRepositoryImpl, then implement the methods as shown in the code below:
infrastructure/repository/FitnessRepositoryImpl
1package com.mongodb.infrastructure.repository
2
3import com.mongodb.MongoException
4import com.mongodb.client.model.Filters
5import com.mongodb.client.model.UpdateOptions
6import com.mongodb.client.model.Updates
7import com.mongodb.domain.entity.Fitness
8import com.mongodb.domain.ports.FitnessRepository
9import com.mongodb.kotlin.client.coroutine.MongoDatabase
10import kotlinx.coroutines.flow.firstOrNull
11import org.bson.BsonValue
12import org.bson.types.ObjectId
13
14class 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}
As observed, the class implements the FitnessRepository interface and includes a companion object. It receives a MongoDatabase instance as a constructor parameter. The companion object defines a constant named FITNESS_COLLECTION, with a value of "fitness," indicating the name of the MongoDB collection where operations such as insert, find, delete, and update are performed on fitness-related data.
If everything is correct, our structure will be as follows:
Project structure

Developing API endpoints

To create our endpoints, understanding routing is crucial. In Ktor, routing dictates how the server handles incoming requests to specific URLs, enabling developers to define actions or responses for each endpoint. Before proceeding with the creation process, let’s briefly review the endpoints we’ll be constructing:
  1. GET /fitness/{id}: Utilize this endpoint to fetch detailed information about a specific fitness activity based on its unique identifier (ID).
  2. POST /fitness: With this endpoint, you can effortlessly add new fitness activities to your tracker, ensuring all your workout data is up-to-date.
  3. PATCH /fitness/{id}: Need to update or modify an existing fitness activity? This endpoint allows you to make targeted changes to specific activities identified by their unique ID.
  4. DELETE /fitness/{id}: Finally, this endpoint empowers you to remove unwanted or outdated fitness activities from your tracker, maintaining a clean and accurate record of your fitness journey.
Now that we know the methods we will implement, let's create our class that will do this work. Inside the application package, let's create a new one called routes and the file below with the name FitnessRoute.kt with the content below:
application/routes/FitnessRoutes.kt
1package com.mongodb.application.routes
2
3import com.mongodb.application.request.FitnessRequest
4import com.mongodb.application.request.toDomain
5import com.mongodb.domain.ports.FitnessRepository
6import io.ktor.http.HttpStatusCode
7import io.ktor.server.application.call
8import io.ktor.server.request.receive
9import io.ktor.server.response.respond
10import io.ktor.server.response.respondText
11import io.ktor.server.routing.route
12import io.ktor.server.routing.Route
13import io.ktor.server.routing.post
14import io.ktor.server.routing.delete
15import io.ktor.server.routing.get
16import io.ktor.server.routing.patch
17import org.bson.types.ObjectId
18import org.koin.ktor.ext.inject
19
20fun 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}
As you can observe, we’ve set up method calls and utilized call to define actions within route handlers. Ktor allows you to manage incoming requests and send responses directly within these handlers, providing flexibility in handling different request scenarios. Additionally, we're utilizing Koin for dependency injection in the FitnessRepository repository.
Now, before we proceed to run the application, we need to install the Koin dependency and incorporate it as a module.
Modules in Ktor are used to organize and encapsulate functionality within your application. They allow you to group related routes, dependencies, and configurations for easier management and maintenance.
To accomplish this, open the Application.kt class and replace all of its code with the one below:
com.mongodb.Application.kt
1package com.mongodb
2
3import com.mongodb.application.routes.fitnessRoutes
4import com.mongodb.domain.ports.FitnessRepository
5import com.mongodb.infrastructure.repository.FitnessRepositoryImpl
6import com.mongodb.kotlin.client.coroutine.MongoClient
7import io.ktor.serialization.gson.gson
8import io.ktor.server.application.Application
9import io.ktor.server.application.install
10import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
11import io.ktor.server.plugins.swagger.swaggerUI
12import io.ktor.server.routing.routing
13import io.ktor.server.tomcat.EngineMain
14import org.koin.dsl.module
15import org.koin.ktor.plugin.Koin
16import org.koin.logger.slf4jLogger
17
18fun main(args: Array<String>): Unit = EngineMain.main(args)
19
20fun 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}
Let’s break down what this code is doing into three main sections for easier understanding:
  1. ContentNegotiation configuration: We’re setting up ContentNegotiation to handle JSON serialization and deserialization. Specifically, we’re using Gson as the default JSON formatter, ensuring seamless communication between our application and clients.
  2. Dependency injection with Koin**:** In the subsequent section, we’re integrating Koin for dependency injection management. Within the first module, we establish the connection to MongoDB, retrieving the URI and database name from the configuration.conf file. Subsequently, we define injection for the FitnessRepository.
  3. Routing configuration: During the routing phase, we configure the Swagger API route, accessible at /swagger-ui. Furthermore, we specify our route for handling Fitness-related operations.
Great. Now, we need to make the final adjustments before running our application. First, open the application.conf file and include the following information:
resources/application.conf
1ktor {
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}
Notice: These variables will be used at the end of the article when we run the application.
Very well. Now, just open the documentation.yaml file and replace it with the content below. In this file, we are indicating which methods our API will provide when accessed through /swagger-ui:
resources/openapi/documentation.yaml
1openapi: 3.0.0
2info:
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
8paths:
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
77components:
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
The final adjustment is to delete the plugins folder, which contains the HTTP.kt , Routing.kt, and Serialization.kt files, as we have already included them in the Application.kt class. Additionally, delete the ApplicationTest class since our focus here is not on tests. We can discuss testing another time. After these changes, your structure should look similar to this:
Project structure

Running the application

To run our application, we need a connection string from MongoDB Atlas. If you don't have access yet, create your account.
Once your account is created, access the Overview menu, then Connect, and select Kotlin. After that, our connection string will be available as shown in the image below:
MongoDB Atlas - Cluster connection string
With the connection string in hand, let's return to IntelliJ, open the Application.kt class, and click on the run button, as shown in the image:
Running the application
At this stage, you will notice that our application encountered an error connecting to MongoDB Atlas. This is where we need to provide the MONGO_URI and MONGO_DATABASE defined in the application.conf file:
To do this, simply edit the configuration and include the values as shown in the image below:
1-DMONGO_URI= Add your connection string
2-DMONGO_DATABASE= Add your database name
Click “Apply” and run the application again.
IntelliJ Run Settings
Attention: Remember to change the connection string to your username, password, and cluster in MongoDB Atlas.
Now, simply access localhost:8080/swagger-ui and perform the operations.
Swagger Dashboard
Open the post method and insert an object for testing:
Swagger Responses
We can see the data recorded in our MongoDB Atlas cluster as shown in the image below:
MongoDB Atlas Document Explorer

Conclusion

MongoDB Atlas, Ktor, and Kotlin API service together form a powerful combination for building robust and scalable applications. MongoDB Atlas provides a cloud-based database solution that offers flexibility, scalability, and reliability, making it ideal for modern applications. Ktor, with its lightweight and asynchronous nature, enables rapid development of web services and APIs in Kotlin, leveraging the language’s concise syntax and powerful features. When combined, MongoDB Atlas and Ktor in Kotlin allow developers to build high-performance APIs with ease, providing seamless integration with MongoDB databases while maintaining simplicity and flexibility in application development. This combination empowers developers to create modern, data-driven applications that can easily scale to meet evolving business needs.
The example source code used in this series is available in Github
Any questions? Come chat with us in the MongoDB Developer Community.
Top Comments in Forums
Forum Commenter Avatar
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).

See More on Forums

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Tutorial

Serverless Development with Kotlin, AWS Lambda, and MongoDB Atlas


Aug 01, 2023 | 6 min read
Tutorial

Beyond Basics: Enhancing Kotlin Ktor API With Vector Search


Sep 18, 2024 | 9 min read
Tutorial

Getting Started Guide for Kotlin Multiplatform Mobile (KMM) with Flexible Sync


Jan 26, 2023 | 8 min read
Tutorial

Getting Started With Server-side Kotlin and MongoDB


Oct 08, 2024 | 6 min read
Table of Contents