Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Serverless Development with Kotlin, AWS Lambda, and MongoDB Atlas

TwitterFacebookRedditLinkedInHacker News

As seen in a previous tutorial, creating a serverless function for AWS Lambda with Java and MongoDB isn’t too complicated of a task. In fact, you can get it done with around 35 lines of code!

However, maybe your stack doesn’t consist of Java, but instead Kotlin. What needs to be done to use Kotlin for AWS Lambda and MongoDB development? The good news is not much will be different!

In this tutorial, we’ll see how to create a simple AWS Lambda function. It will use Kotlin as the programming language and it will use the MongoDB Kotlin driver for interacting with MongoDB.

The requirements

There are a few prerequisites that must be met in order to be successful with this particular tutorial:

  • Must have a Kotlin development environment installed and configured on your local computer.
  • Must have a MongoDB Atlas instance deployed and configured.
  • Must have an Amazon Web Services (AWS) account.

The easiest way to develop with Kotlin is through IntelliJ, but it is a matter of preference. The requirement is that you can build Kotlin applications with Gradle.

For the purpose of this tutorial, any MongoDB Atlas instance will be sufficient whether it be the M0 free tier, the serverless pay-per-use tier, or something else. However, you will need to have the instance properly configured with user rules and network access rules. If you need help, use our MongoDB Atlas tutorial as a starting point.

Defining the project dependencies with the Gradle Kotlin DSL

Assuming you have a project created using your tooling of choice, we need to properly configure the build.gradle.kts file with the correct dependencies for AWS Lambda with MongoDB.

In the build.gradle.kts file, include the following:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.9.0"
    application
    id("com.github.johnrengelman.shadow") version "7.1.2"
}

application {
    mainClass.set("example.Handler")
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test"))
    implementation("com.amazonaws:aws-lambda-java-core:1.2.2")
    implementation("com.amazonaws:aws-lambda-java-events:3.11.1")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
    implementation("org.mongodb:bson:4.10.2")
    implementation("org.mongodb:mongodb-driver-kotlin-sync:4.10.2")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
}

There are a few noteworthy items in the above configuration.

Looking at the plugins first, you’ll notice the use of Shadow:

plugins {
    kotlin("jvm") version "1.9.0"
    application
    id("com.github.johnrengelman.shadow") version "7.1.2"
}

AWS Lambda expects a ZIP or a JAR. By using the Shadow plugin, we can use Gradle to build a “fat” JAR, which includes both the application and all required dependencies. When using Shadow, the main class must be defined.

To define the main class, we have the following:

application {
    mainClass.set("example.Handler")
}

The above assumes that all our code will exist in a Handler class in an example package. Yours does not need to match, but note that this particular class and package will be referenced throughout the tutorial. You should swap names wherever necessary.

The next item to note is the dependencies block:

dependencies {
    testImplementation(kotlin("test"))
    implementation("com.amazonaws:aws-lambda-java-core:1.2.2")
    implementation("com.amazonaws:aws-lambda-java-events:3.11.1")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
    implementation("org.mongodb:bson:4.10.2")
    implementation("org.mongodb:mongodb-driver-kotlin-sync:4.10.2")
}

In the above block, we are including the various AWS Lambda SDK packages as well as the MongoDB Kotlin driver. These dependencies will allow us to use MongoDB with Kotlin and AWS Lambda.

If you wanted to, you could run the following command:

./gradlew shadowJar

As long as the main class exists, it should build a JAR file for you.

Developing a serverless function with Kotlin and MongoDB

With the configuration items out of the way, we can focus on the development of our serverless function. Open the project’s src/main/kotlin/example/Handler.kt file and include the following boilerplate code:

package example

import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
import com.mongodb.client.model.Filters
import com.mongodb.kotlin.client.MongoClient
import com.mongodb.kotlin.client.MongoCollection
import com.mongodb.kotlin.client.MongoDatabase
import org.bson.Document
import org.bson.conversions.Bson
import org.bson.BsonDocument

class Handler : RequestHandler<Map<String, String>, Void> {

    override fun handleRequest(input: Map<String, String>, context: Context): void {

        return null;

    }
}

The above code won’t do much of anything if you tried to execute it on AWS Lambda, but it is a starting point. Let’s start by establishing a connection to MongoDB.

Within the Handler class, add the following:

class Handler : RequestHandler<Map<String, String>, Void> {

    private val mongoClient: MongoClient = MongoClient.create(System.getenv("MONGODB_ATLAS_URI"))

    override fun handleRequest(input: Map<String, String>, context: Context): void {

        val database: MongoDatabase = mongoClient.getDatabase("sample_mflix")
        val collection: MongoCollection<Document> = database.getCollection("movies")

        return null;

    }
}

First, you’ll notice that we are creating a mongoClient variable to hold the information about our connection. This client will be created using a MongoDB Atlas URI that we plan to store as an environment variable. It is strongly recommended that you use environment variables to store this information so your credentials don’t get added to your version control.

In case you’re unsure what the MongoDB Atlas URI looks like, it looks like the following:

mongodb+srv://<username>:<password>@<clustername>.dmhrr.mongodb.net/?retryWrites=true&w=majority

You can find your exact connection string using the MongoDB Atlas CLI or through the MongoDB Atlas dashboard.

Within the handleRequest function, we get a reference to the database and collection that we want to use:

val database: MongoDatabase = mongoClient.getDatabase("sample_mflix")
val collection: MongoCollection<Document> = database.getCollection("movies")

For this particular example, we are using the sample_mflix database and the movies collection, both of which are part of the optional MongoDB Atlas sample dataset. Feel free to use a database and collection that you already have.

Now we can focus on interactions with MongoDB. Make a few changes to the Handler class so it looks like this:

package example

import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
import com.mongodb.client.model.Filters
import com.mongodb.kotlin.client.MongoClient
import com.mongodb.kotlin.client.MongoCollection
import com.mongodb.kotlin.client.MongoDatabase
import org.bson.Document
import org.bson.conversions.Bson
import org.bson.BsonDocument

class Handler : RequestHandler<Map<String, String>, List<Document>> {

    private val mongoClient: MongoClient = MongoClient.create(System.getenv("MONGODB_ATLAS_URI"))

    override fun handleRequest(input: Map<String, String>, context: Context): List<Document> {

        val database: MongoDatabase = mongoClient.getDatabase("sample_mflix")
        val collection: MongoCollection<Document> = database.getCollection("movies")

        var filter: Bson = BsonDocument()

        if(input.containsKey("title") && !input.get("title").isNullOrEmpty()) {
            filter = Filters.eq("title", input.get("title"))
        }

        val results: List<Document> = collection.find(filter).limit(5).toList()

        return results;

    }
}

Instead of using Void in the RequestHandler and void as the return type for the handleRequest function, we are now using List<Document> because we plan to return an array of documents to the requesting client.

This brings us to the following:

var filter: Bson = BsonDocument()

if(input.containsKey("title") && !input.get("title").isNullOrEmpty()) {
    filter = Filters.eq("title", input.get("title"))
}

val results: List<Document> = collection.find(filter).limit(5).toList()

return results;

Instead of executing a fixed query when the function is invoked, we are accepting input from the user. If the user provides a title field with the invocation, we construct a filter for it. In other words, we will be looking for movies with a title that matches the user input. If no title is provided, we just query for all documents in the collection.

For the actual find operation, rather than risking the return of more than a thousand documents, we are limiting the result set to five and are converting the response from a cursor to a list.

At this point in time, our simple AWS Lambda function is complete. We can focus on the building and deployment of the function now.

Building and deploying a Kotlin function to AWS Lambda

Before we worry about AWS Lambda, let’s build the project using Shadow. From the command line, IntelliJ, or with whatever tool you’re using, execute the following:

./gradlew shadowJar

Find the JAR file, which is probably in the build/libs directory unless you specified otherwise.

Everything we do next will be done in the AWS portal. There are three main items that we want to take care of during this process:

  1. Add the environment variable with the MongoDB Atlas URI to the Lambda function.
  2. Rename the “Handler” information in Lambda to reflect the actual project.
  3. Upload the JAR file to AWS Lambda.

Within the AWS Lambda dashboard for your function, click the “Configuration” tab followed by the “Environment Variables” navigation item. Add MONGODB_ATLAS_URI along with the appropriate connection string when prompted. Make sure the connection string reflects your instance with the proper username and password.

You can now upload the JAR file from the “Code” tab of the AWS Lambda dashboard. When this is done, we need to tell AWS Lambda what the main class is and the function that should be executed.

In the “Code” tab, look for “Runtime Settings” and choose to edit it. In our example, we had example as the package and Handler as the class. We also had our function logic in the handleRequest function.

With all this in mind, change the “Handler” within AWS Lambda to example.Handler::handleRequest or whatever makes sense for your project.

At this point, you should be able to test your function.

On the “Test” tab of the AWS Lambda dashboard, choose to run a test as is. You should get a maximum of five results back. Next, try using the following input criteria:

{
	"title": "The Terminator"
}

Your response will now look different because of the filter.

Conclusion

Congratulations! You created your first AWS Lambda function in Kotlin and that function supports communication with MongoDB!

While this example was intended to be short and simple, you could add significantly more logic to your functions that engage with other functionality of MongoDB, such as aggregations and more.

If you’d like to see how to use Java to accomplish the same thing, check out my previous tutorial on the subject titled Serverless Development with AWS Lambda and MongoDB Atlas Using Java.

This content first appeared on MongoDB.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.