Ktor server for beginners – MongoDB (Part 1 – CRUD)

Level: Beginner

In the previous lesson we made our first GET and POST requests and we used a mutable list of fruits to simulate adding and retrieving data from a database, in this lesson we are going to save our data in a real database as this will be much more convenient 🙂

What is MongoDB?

MongoDB is a popular NoSQL database that is designed for storing and retrieving large volumes of unstructured and semi-structured data. Unlike traditional SQL databases, MongoDB uses a document-based data model, where data is stored in flexible and dynamic JSON-like documents instead of tables and rows.

Document based model?

In MongoDB, data is stored as documents, which are similar to JSON objects. Each document is a self-contained unit of data that can contain multiple fields, arrays, and nested objects. Documents are stored in collections, which are similar to tables in a traditional SQL database.

Unlike traditional SQL databases, MongoDB does not use a fixed schema or structure for storing data. This means that documents within the same collection can have different fields and data types, and fields can be added or removed from documents at any time.

Documents in MongoDB are identified by a unique key called the “_id” field. This field is automatically generated by MongoDB when a new document is inserted into a collection, but it can also be manually specified by the developer.

Prerequisites

Dependencies

For this tutorial im going to use the same project from the previous lesson.

Open the build.gradle file in the root of your project and add the following dependencies for Kmongo (latest version here)

dependencies {
    implementation("org.litote.kmongo:kmongo:4.8.0")
    implementation("org.litote.kmongo:kmongo-coroutine:4.8.0")
}

Creating the database

In the “data” package create a new package called “db” then create a new kotlin file called Database, we will define our MongoDB instance and our collections here, off course you can replace “tahaben_db” with whatever you would like to name your database.

import org.litote.kmongo.coroutine.coroutine
import org.litote.kmongo.reactivestreams.KMongo

val db = KMongo.createClient().coroutine.getDatabase("tahaben_db")

Now let’s think about what collections do we need, since we’re going to work with a list of fruits we will need a fruit collection, before we define our fruit collection let’s make a small edit to our Fruit data class, we will mark the id attribute with “@BsonId”, since MongoDB needs and id for each item lets use our fruit id to be that id, this is not necessary as MongoDB will automatically create a “_id” field if we don’t specify it but that will just add an unnecessary extra value to each fruit we add to our collection as we will have both “id” and “_id” for each fruit. Another thing since “@BsonId” does not support serialization we will fix this by providing a default value of “ObjectId” to the id, this way serialization will work fine and an id will be auto generated if we don’t specify it.

    @BsonId
    val id: String = ObjectId().toString()

now back to our Database.kt file let’s define our fruit collection.

val fruits = db.getCollection<Fruit>()

Now that we defined our collection we would like to perform some actions on it like getting a list of fruits and adding a new fruit we can do that by implementing functions in the same file, we use the find function to return all documents in the collection then we call toList to convert it to a kotlin list, to insert items we use insertOne function and we pass the fruit that we want to add, also wasAcknowlendged will return true if the fruit was added successfully to the database or false if there is an error.

suspend fun addFruit(newFruit: Fruit): Boolean {
    return try{
        // return true if insertion was successful
        fruits.insertOne(newFruit).wasAcknowledged()
    }catch (ex: Exception){
        ex.printStackTrace()
        // return false if there is an error EX: item with same id already exists
        false
    }
}

suspend fun getFruits(): List<Fruit> {
    return fruits.find().toList()
}

To use our new functions in our API we need to go to the FruitRoutes.kt file and change the logic in our endpoints to use them.

fun Route.fruitRoutes() {

    get("/fruits/{id?}") {
        // get fruits from the database
        call.respond(HttpStatusCode.OK, getFruits())
    }
    post("/add-fruit") {
        try {
            // receive the fruit from the user
            val newFruit = call.receive<Fruit>()

            // add the received fruit to the database
            if (!addFruit(newFruit)){
                // if not added successfully return with an error
                return@post call.respond(HttpStatusCode.Conflict, SimpleResponse(success = false, message = "Item already exits"))
            }

            // acknowledge that we successfully added the fruit by responding
            call.respond(
                HttpStatusCode.Created, SimpleResponse(
                    true,
                    "Successfully added ${newFruit.name}"
                )
            )
        }catch (ex: Exception){

            call.respond(HttpStatusCode.BadRequest, SimpleResponse(false,"Invalid Fruit format"))
        }

    }
}

Start the MongoDB server and try our end points in postman

Open MongoDB Compass find your database name and check out the items you inserted

Deleting and updating data

To delete and update items in our database we can use updateOneById and deleteOneById functions, here is an example of deleting and updating fruits

suspend fun deleteFruit(fruitId: String): Boolean{
    return try {
        fruits.deleteOneById(fruitId).deletedCount == 1L
    }catch (ex: Exception){
        ex.printStackTrace()
        false
    }
}

suspend fun updateFruit(updatedFruit: Fruit): Boolean {
    return try {
        fruits.updateOneById(id = updatedFruit.id ,update = updatedFruit).wasAcknowledged()
    }catch (ex: Exception){
        ex.printStackTrace()
        false
    }
}

And we define the DELETE and PATCH end points in FruitRoutes.kt

    patch("/add-fruit") {
        try {
            val newFruit = call.receive<Fruit>()
            if (!updateFruit(newFruit)) {
                return@patch call.respond(
                    HttpStatusCode.NotFound,
                    SimpleResponse(success = false, message = "Please check the fruit id")
                )
            }
            call.respond(
                HttpStatusCode.OK, SimpleResponse(
                    true,
                    "Successfully updated ${newFruit.name}"
                )
            )
        } catch (ex: Exception) {
            call.respond(HttpStatusCode.BadRequest, SimpleResponse(false, "Invalid Fruit format"))
        }
    }

    delete("/delete-fruit/&{id?}") {
        try {
            val id = call.parameters["id"]
                ?: return@delete call.respond(
                    HttpStatusCode.BadRequest,
                    SimpleResponse(success = false, message = "Parameter id is required")
                )
            if (!deleteFruit(id)) {
                return@delete call.respond(
                    HttpStatusCode.NotFound,
                    SimpleResponse(success = false, message = "Please check the fruit id")
                )
            }
            call.respond(
                HttpStatusCode.OK, SimpleResponse(
                    true,
                    "Successfully deleted ${id}"
                )
            )
        } catch (ex: Exception) {
            call.respond(HttpStatusCode.BadRequest, SimpleResponse(false, "Invalid Fruit format"))
        }
    }

Give our new end points a try, the update endpoint is the same as add but don’t forget to change the request type to PATCH in post man before you try it out, the delete end point is diffrent and requires an id try it with and without a parameter and notice how the server returns BadRequest.

That’s it for this tutorial! in the next tutorial we are going to learn how to make different queries to MongoDB.

The source code for this tutorial is available here.

5 1 vote
Article Rating
Subscribe
Notify of
guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

[…] back!, in the previous lesson we learned how to setup our mongoDB and how to add, read, update and delete documents from our […]