Ktor + MongoDB: Your Recipe for Building a Strong Backend

Featured on Hashnode
Ktor + MongoDB: Your Recipe for Building a Strong Backend

Hello, Devs I wanted to share my recent experience with you. I created a backend using Ktor and MongoDB, but I struggled to find resources that could help me integrate the official MongoDB driver with Ktor. I know that many of us have faced similar challenges, especially with the deprecation of KMong. However, I didn’t let that stop me! I decided to create a detailed blog post that will walk you through the process of using the official MongoDB driver in your Ktor projects.

Step 1: Add dependency in Gradle

//other code block

dependencies {
    implementation("io.ktor:ktor-server-core-jvm")
    implementation("io.ktor:ktor-server-auth-jvm")
    implementation("io.ktor:ktor-server-call-logging-jvm")
    implementation("io.ktor:ktor-server-content-negotiation-jvm")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
    implementation("io.ktor:ktor-server-netty-jvm")
    implementation("ch.qos.logback:logback-classic:$logback_version")

    //MongoDb
    implementation("org.mongodb:mongodb-driver-kotlin-coroutine:4.10.1")
}
  1. Ktor-server-core-jvm: This dependency forms the core foundation of your Ktor server application, providing the necessary infrastructure for handling HTTP requests and responses.

  2. Ktor-server-auth-jvm: Enables authentication mechanisms within your Ktor server, ensuring secure access control to your endpoints.

  3. Ktor-server-call-logging-jvm: Facilitates logging of incoming HTTP requests and outgoing responses, aiding in debugging and monitoring efforts.

  4. Ktor-server-content-negotiation-jvm: Enables content negotiation, allowing your server to respond with different data formats based on client preferences.

  5. Ktor-serialization-kotlinx-json-jvm: Integrates Kotlinx Serialization with Ktor, facilitating JSON serialization and deserialization for seamless data exchange.

  6. Ktor-server-netty-jvm: Utilizes the Netty engine as the underlying HTTP server for your Ktor application, offering high performance and scalability.

  7. Logback-classic: Integrates the Logback logging framework into your application, providing rich logging capabilities.

  8. Mongodb-driver-kotlin-coroutine: This dependency declaration is used to include the MongoDB Kotlin Coroutine extension for the official MongoDB driver in your Ktor project

Step 2: Application class the entry point of the Ktor project

fun main(args: Array<String>) {
    io.ktor.server.netty.EngineMain.main(args)
}

@Suppress("unused")
fun Application.module() {
    configureMongoDatabase()
    configureMonitoring()
    configureSerialization()
    configureRouting()
}

In this code, we have the entry point of a Ktor server application defined in the fun main(args: Array<String>) function. This function sets up the server by invoking io.ktor.server.netty.EngineMain.main(args), which starts the Netty server engine.

The @Suppress("unused") annotation precedes the fun Application.module() function, which is the main configuration function for our Ktor application. Within this function, we configure various aspects of our server application:

  • configureMongoDatabase(): This function is responsible for setting up the MongoDB database connection. It ensures that our Ktor application can seamlessly interact with the MongoDB database, enabling the storage and retrieval of data.

  • configureMonitoring(): Here, we configure monitoring features for our application, which may include metrics collection, health checks, and performance monitoring. This ensures that our application remains healthy and performs optimally during runtime.

  • configureSerialization(): This function sets up serialization and deserialization capabilities using Kotlinx Serialization. It enables our application to easily convert data between Kotlin objects and various formats like JSON.

  • configureRouting(): Finally, this function configures the routing for our application, defining the endpoints and their corresponding handlers. It determines how incoming requests are processed and responded to by our Ktor server.

Step 3: Configure the application


ktor {
    deployment {
        port = 8080
        port = ${?PORT}
        host = "Domain name or ip address"
    }
    application {
        modules = [ com.example.ApplicationKt.module ]
    }
    mongo {
        uri = ${?MONGO_URI}
        database = ${?MONGO_DATABASE}
    }
}

In this setup, we configure a Ktor server application in conjunction with MongoDB integration. The configuration includes essential deployment and application settings, ensuring high server performance and flexibility. We establish the foundation for managing incoming requests and directing them to the correct endpoints by defining the deployment port, host, and application modules.

Furthermore, the configuration plays a critical role in configuring the MongoDB connection, allowing for efficient interaction with the database. Developers can easily connect their Ktor application to a MongoDB instance using the “mongoUri” and “dbName” parameters, enabling simple storage and retrieval of data.

Step 4: Now Add a Plugin (Basically this is an extension function of the application)

fun Application.configureMongoDatabase(): MongoDatabase {
    val client = MongoClient.create(connectionString = System.getenv("MONGO_URI"))
    return  client.getDatabase(databaseName = "MONGO_DATABASE")
}

fun Application.configureMonitoring() {
    install(CallLogging) {
        level = Level.INFO
        filter { call -> call.request.path().startsWith("/") }
    }
}

fun Application.configureRouting() {
    val userDataSource = MongoUserDataSource(configureMongoDatabase())
    routing {
       userRouting(userDataSource)
    }
}

This code showcases the configuration of a Ktor application with MongoDB integration, highlighting essential components such as database setup, monitoring, routing, and WebSocket communication.

  • configureMongoDatabase(): This function initializes the MongoDB database connection using environment variables for the connection string and database name. It returns an MongoDatabase instance for interacting with the MongoDB database.

  • configureMonitoring(): Here, we install the CallLogging feature to log incoming requests at the INFO level, filtering requests to paths starting with "/". This facilitates comprehensive monitoring of server activity.

  • configureRouting(): In this function, we set up routing for user-related endpoints and chat functionalities using MongoDB data sources. This lays the groundwork for seamless data handling and management within the Ktor application.

Step 5: Create a Model class for the User

@Serializable
data class User(
    @BsonId
    val userId: String = ObjectId().toString(),
    val username: String,
    val userProfilePhoto: String?,
    val email: String,
    val phone: String,
    val password: String
)

The User data class represents a user entity within the backend application, annotated with @Serializable for easy serialization and deserialization.

@BsonId: Annotates the userId property as the BSON identifier for MongoDB, ensuring uniqueness within the database collection.

Step 6: Create the MongoDB Data Source

interface UserDataSource {
    suspend fun getUsers(): List<User>?
    suspend fun getUserById(userId: String): User?
    suspend fun registerUser(user: User): Boolean
}

The UserDataSource interface outlines the contract for interacting with user data within the backend application. It defines a set of suspend functions for performing asynchronous operations related to user management.

By defining these suspend functions in the UserDataSource interface, developers can implement custom data source classes that interact with various data storage systems, such as databases or external APIs, while ensuring asynchronous and non-blocking behaviour within the Ktor application.

Step 7: Create an Implementation class of MongoDB Data Source

class MongoUserDataSource(
    mongoDatabase: MongoDatabase
): UserDataSource {

    companion object {
        const val USER_COLLECTION = "collection_name"
    }

    val users = mongoDatabase.getCollection<User>(USER_COLLECTION)
    var user: User? = null
    override suspend fun getUsers(): List<User> {
        return users.find<User>().toList()
    }

    override suspend fun getUserById(userId: String): User? {

        users.find<User>(filter = Filters.eq(User::userId.name,userId)).filterNotNull().collect{
            user = it
        }
        return user
    }

   override suspend fun registerUser(user: User): Boolean {
         return users.insertOne(user).wasAcknowledged()
    }
}

The MongoUserDataSource class implements the UserDataSource interface, providing functionality to interact with user data stored in a MongoDB database. This class leverages the MongoDB Kotlin Coroutine extension for asynchronous database operations.

Step 8: Create the route function for User

fun Route.userRouting(
    userDataSource: MongoUserDataSource
) {
    route("/user"){
        get {
            val user = userDataSource.getUsers()
            if(user.isNotEmpty()){
                call.respond(user)
            }else{
                call.respondText("No user found", status = HttpStatusCode.OK)
            }
        }

        get("/byId/{id?}"){
            val id = call.parameters["id"] ?: return@get call.respondText("Missing id", status = HttpStatusCode.BadRequest)
            val user = userDataSource.getUserById(id) ?: return@get call.respondText("No User Found", status = HttpStatusCode.NotFound)
            call.respond(user)
        }

        post("/register"){
            val user = call.receive<User>()
            userDataSource.registerUser(user)
            call.respondText("User Registered", status = HttpStatusCode.Created)
        }
}

The userRouting function defines routes for user-related endpoints within the Ktor application, facilitating user management operations such as fetching user data and user registration. This function leverages the MongoUserDataSource class for interacting with user data stored in a MongoDB database.

  • get("/"): Handles HTTP GET requests to retrieve all users. If users are found, their details are returned in the response; otherwise, a "No user found" message is sent.

  • get("/byId/{id?}"): Handles HTTP GET requests to retrieve a user by their unique identifier. If the user is found, their details are returned in the response; otherwise, a "No user found" message is sent.

  • post("/register"): Handles HTTP POST requests to register a new user. The user data is received from the request body and registered using the MongoUserDataSource instance. Upon successful registration, a "User Registered" message is sent in the response with a status code of 201 (Created).


Connect with Me:

Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? 😊 You can also follow my Instagram page for more mobile development-related content. 📲👨‍💻 Let’s stay connected, share knowledge and have some fun in the exciting world of app development! 🌟

Check out my Instagram page

Check out my linkedIn