Kotlin + Jersey + Jetty + MongoDB – creating a scalable RESTful API

Hi! I spent a day making Jetty, Jersey, and MongoDB work together in Kotlin, that's why you won't have to!

Intro:

We're working on Tokens 2 now, and it needs a robust REST API that's easy to update. We love using Kotlin for our APIs, since it provides a lot of modern syntactic sugar, while being based on Java, which has a ton of libraries available and is as reliable as it gets.

We were using plain servlets before, since our APIs weren't RESTful. This time we need to be able to manipulate all the objects from the client, so RESTful it is. We wanted something that's easy to write and lets us avoid writing boilerplate code every time. That's why we picked up Jersey for this task, as it allows to easily create standard GET / POST / PUT / DELETE interactions for data models, while also allowing for any customization.

We use Jetty for serving multiple web apps in our company. For the ease of use and deployment, Jetty is installed on our server permanently and we're not embedding it into the webapps themselves. All the config guides I found online told only the "embedded Jetty" story, which doesn't work out for our use case.

If you're like us a little – welcome to this guide, I'll take you through all the steps needed to make your API run in less than 30 minutes.

Guide:

1.   Create a Kotlin project. We use Gradle, but you can use Maven if you want. You can also use my template.

2.   Here's the list of dependencies you'll need in addition to your usual ones:

dependencies {    
	compile 'org.mongodb:mongodb-driver-sync:3.10.0'    
	compile 'org.litote.kmongo:kmongo:3.9.2'    
	compile 'org.litote.kmongo:kmongo-id-jackson'    
	compile 'org.litote.kmongo:kmongo-id:3.9.2'
	compile 'org.glassfish.jersey.core:jersey-server:2.30'    
	compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.30'
    compile 'org.glassfish.jersey.inject:jersey-hk2:2.30'    
	compile 'org.glassfish.jersey.media:jersey-media-json-jackson:2.30'
}
x

3.   Create a web.xml at src/main/webapp/WEB-INF/web.xml

4.   Use this snippet to populate your web.xml. Replace the %%'s with your info.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
    <display-name>%%YourProjectName%%</display-name>
    <servlet>
        <servlet-name>Jersey REST Service</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>%%your.package.here%%</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Jersey REST Service</servlet-name>
        <url-pattern>/api/*</url-pattern>
    </servlet-mapping>
</web-app>

5.   We need to register a @Provider for the KMongo. It will ensure the conversion is seamless between the JSON objects and the KMongo Data Classes we will create next.

Create a KMongoProvider.kt and populate it with this code:

@Provider
class KMongoProvider : ContextResolver<ObjectMapper> {
    override fun getContext(type: Class<*>): ObjectMapper {
        return KMongoJackson.mapper
    }
}

object KMongoJackson {
    val mapper = jacksonObjectMapper()

    init {
        mapper.registerModule(IdJacksonModule())
    }
}

6.   Let's connect to the database and create a data model:

class Connect {
	companion object {
		val client = KMongo.createClient()
		val database = client.getDatabase("kotlin-example")
	}
}

data class Dog (
        @BsonId var _id: StringId<Dog> = newStringId(),
        var owner: String,
        var name: String,
        var bornAt: Int,
        var lastVaccineAt: Int?
) {
    companion object

	fun collection(): MongoCollection<Dog> {
        return Connect.database.getCollection<Dog>("dogs")
    }
}

7.   Now we need to have a resource for our Dog class. It will tell Jersey how to interact with the model and it will expose the model for the API access:

@Path("dogs")
@Produces(APPLICATION_JSON)
class DogResource {

    @GET
    fun listDogs(): MongoIterable<Dog> {
        return Dog.collection().find().map { it }
    }

    @POST
    fun createDog(token: Dog) {
        Dog.collection().save(token)
    }

    @GET
    @Path("{id}")
    fun getDog(@PathParam("id") id: String): Dog? {
        return Dog.collection().findById(id)
    }

    @PUT
    @Path("{id}")
    fun updateDog(@PathParam("id") id: String, dog: Dog) {
        dog._id = StringId(id)
        Dog.collection().replaceById(id, dog)
    }

    @DELETE
    @Path("{id}")
    fun deleteDog(@PathParam("id") id: String): Boolean {
        return Dog.collection().deleteById(id).deletedCount > 0
    }

}

The listDogs() method takes no parameters and uses the path we defined for the whole DogResource. The createDog() method takes a JSON that should match the data model, but without the _id key. Try to send wrong format of JSON to see how it behaves. The get / update / delete methods work on */dogs/{id}/.

8.   I use extension functions listed below to easily find / replace stuff by id. Create a DBUtility.kt and put the functions in it.

fun <T> MongoCollection<T>.findById(id: String): T? {
	return this.findOneById(StringId<T>(id))
}

fun <T> MongoCollection<T>.deleteById(id: String): DeleteResult {
	return this.deleteOneById(StringId<T>(id))
}

fun <T> MongoCollection<T>.replaceById(id: String, newObject: T): UpdateResult {
	return this.replaceOne(KMongoUtil.idFilterQuery(StringId<T>(id)), newObject)
}

9.   Let's test it now.

I'm using Postman to send a POST request to
localhost:8080/api/dogs/. Here's its body:

{
	"owner": "John Doe",
    "name": "Good Boy",
    "bornAt": 1583158516
}

Then if you GET localhost:8080/api/dogs/ – you'll see the newly created object.

Now you can add as many resources as you want in a matter of minutes, you don't have to do much for it. You can also use custom paths and define whatever methods you want, Jersey will always handle the parsing and actual servlet creation for you.

It scales as good as any other servlet app, while can also be deployed to other servers like Tomcat. I love server-side Kotlin in comparison to something like Node.js. While it requires a little more initial setup – you can greatly benefit from using it later on. Two of the most important things for us are proper multithreading support and the inherent need to structure the code properly.

I really hope it helps you and saves you some time. If you encounter any issues with this setup – please contact me and I'll update the project accordingly.