Tutorial: Simple Kotlin REST API with Ktor, Exposed and Kodein

When building a REST API, usually Spring (Boot) is one of the first frameworks that comes to my mind. It has proven to be a reliable framework, but it also has steep learning path and a lot of overhead for smaller projects.

Also, when using Kotlin instead of Java you feel that Spring does not make use of many cool language features that are available.

This is why I started looking for alternatives and I found Ktor. It makes extensive use of Kotlin features and allows to quickly setup a REST API without having too much configuration to do before you can start.

In this tutorial I show you how to setup a REST API with Ktor, but also create a scalable project structure that can be extended easily. Beside Ktor we will be using Exposed as ORM (Object-relational mapping) framework, and Kodein for DI (Dependency Injection).

The code for this repository is also available on Github: https://github.com/stefangaller/Ktor-Api.

Prerequesites

As a first step we need to create a new Ktor project. I suggest that you follow the official “Quick Start” documentation. This should get you a basic Ktor application from which we will start building our API.

Database

For our database I use Docker to quickly create a PostgreSQL instance. My docker compose file looks like this:

version: "3.7" services: books_db: image: postgres:13-alpine environment: POSTGRES_DB: "bookdb" POSTGRES_USER: "user" POSTGRES_PASSWORD: "password" POSTGRES_ROOT_PASSWORD: "rootpwd" ports: - "5432:5432"

We will use Hikari as JDBC connection pool and therefore create a configuration file called dbconfig.properties under the resources folder. The dbconfig.properties file should look like this:

dataSourceClassName=org.postgresql.ds.PGSimpleDataSource dataSource.user=user dataSource.password=password dataSource.databaseName=bookdb dataSource.portNumber=5432 dataSource.serverName=localhost

Just remember to adapt this file when using a different database.

Next, we add the path of the dbconfig.properties file to our application.conf file which is also located under resources. This step is not necessary, but it allows us to potentially have a separate database configuration for each application configuration (e.g. for development, staging and production environments).

ktor { ... hikariconfig = "resources/dbconfig.properties" }

Dependencies

Let’s have a look at the dependencies in our build.gradle file. My dependencies block looks like this:

dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // kotlin implementation "io.ktor:ktor-server-netty:$ktor_version" // ktor netty server implementation "ch.qos.logback:logback-classic:$logback_version" //logging implementation "io.ktor:ktor-server-core:$ktor_version" // ktor server implementation "io.ktor:ktor-gson:$ktor_version" // gson for ktor implementation "org.kodein.di:kodein-di-framework-ktor-server-jvm:7.0.0" // kodein for ktor // Exposed ORM library implementation "org.jetbrains.exposed:exposed-core:$exposed_version" implementation "org.jetbrains.exposed:exposed-dao:$exposed_version" implementation "org.jetbrains.exposed:exposed-jdbc:$exposed_version" implementation "com.zaxxer:HikariCP:3.4.5" // JDBC Connection Pool implementation "org.postgresql:postgresql:42.2.1" // JDBC Connector for PostgreSQL testImplementation "io.ktor:ktor-server-tests:$ktor_version" // test framework }

I do not cover testing in this tutorial, so feel free to remove the last dependency. Also remember to add a different JDBC Connector if you are using another database like MySQL.

To make the example work, I also had to add following block to my build.gradle to make sure the project is compiled for JVM 1.8:

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { jvmTarget = "1.8" } }

Data Persistence with Exposed

For this tutorial we will implement a simple API to add, delete and retrieve books. So let’s have a look how we can use Exposed for our database transactions.

In our src directory create a new package called data. Here we a new Kotlin file Book.kt that will contain all our logic for adding, deleting and retrieving books from our database.

Book.kt

package at.stefangaller.data import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable object Books : IntIdTable() { val title = varchar("title", 255) val author = varchar("author", 255) } class BookEntity(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<BookEntity>(Books) var title by Books.title var author by Books.author override fun toString(): String = "Book($title, $author)" fun toBook() = Book(id.value, title, author) } data class Book( val id: Int, val title: String, val author: String )

Service Layer using Exposed

Now that we have defined our data model and configured Exposed to correctly map the Book class to the database, we create a BookService containing functions for manipulating the data.

In the src directory create a services package and add a new BookService.kt file to it.

BookService.kt

package at.stefangaller.services import at.stefangaller.data.Book import at.stefangaller.data.BookEntity import org.jetbrains.exposed.sql.transactions.transaction class BookService { fun getAllBooks(): Iterable<Book> = transaction { BookEntity.all().map(BookEntity::toBook) } fun addBook(book: Book) = transaction { BookEntity.new { this.title = book.title this.author = book.author } } fun deleteBook(bookId: Int) = transaction { BookEntity[bookId].delete() } }

Note that all function code is wrapped within a transaction. All database operations using Exposed are required to be executed in a transaction block.

Provide BookService using Kodein

To make the BookService available throughout the app we are using the Kodein library. Within the services package create a new file called ServicesDI.kt.

package at.stefangaller.services import org.kodein.di.DI import org.kodein.di.bind import org.kodein.di.singleton fun DI.MainBuilder.bindServices(){ bind<BookService>() with singleton { BookService() } }

All we do here is to create an extension function for the DI.MainBuilder class named bindServices and bind our BookService as a singleton.

Routing: Defining the REST API

Now we need to define how our REST API will look like. Therefore, we create a new package called routes in our src directory.

We are going to split up our route definition in two files: ApiRoute and BookRoute. ApiRoute will be our base route defining our API (represented by the path /api/v1). The BookRoute is responsible for all API endpoints regarding books.

Since in the future we might also have have an AuthorRoute or a PublisherRoute it makes sense to use that structure.

Let’s have a look at the ApiRoute first:

ApiRoute.kt

package at.stefangaller.routes import io.ktor.routing.Routing import io.ktor.routing.route fun Routing.apiRoute() { route("/api/v1") { books() } }

All it does is to create a /api/v1 route and registers the books route beneath it. The books function is defined in BookRoute.kt:

BookRoute.kt

package at.stefangaller.routes import at.stefangaller.data.Book import at.stefangaller.services.BookService import io.ktor.application.call import io.ktor.features.NotFoundException import io.ktor.http.HttpStatusCode import io.ktor.request.receive import io.ktor.response.respond import io.ktor.routing.Route import io.ktor.routing.delete import io.ktor.routing.get import io.ktor.routing.post import org.kodein.di.instance import org.kodein.di.ktor.di fun Route.books() { val bookService by di().instance<BookService>() get("books") { val allBooks = bookService.getAllBooks() call.respond(allBooks) } post("book") { val bookRequest = call.receive<Book>() bookService.addBook(bookRequest) call.respond(HttpStatusCode.Accepted) } delete("book/{id}") { val bookId = call.parameters["id"]?.toIntOrNull() ?: throw NotFoundException() bookService.deleteBook(bookId) call.respond(HttpStatusCode.OK) } }

Connecting to the Database

Before we starting to put everything together, what is still missing is some code to connect to the database when the server is starting. We will do this in a file called DBConfig.kt.

package at.stefangaller import at.stefangaller.data.Books import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import io.ktor.application.Application import io.ktor.util.KtorExperimentalAPI import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.LoggerFactory const val HIKARI_CONFIG_KEY = "ktor.hikariconfig" @KtorExperimentalAPI fun Application.initDB() { val configPath = environment.config.property(HIKARI_CONFIG_KEY).getString() val dbConfig = HikariConfig(configPath) val dataSource = HikariDataSource(dbConfig) Database.connect(dataSource) createTables() LoggerFactory.getLogger(Application::class.simpleName).info("Initialized Database") } private fun createTables() = transaction { SchemaUtils.create( Books ) }

Application.kt: Putting everything together

Finally, all that is left is to connect all the parts in the Application.kt file.

package at.stefangaller import at.stefangaller.routes.apiRoute import at.stefangaller.services.bindServies import io.ktor.application.Application import io.ktor.application.call import io.ktor.application.install import io.ktor.features.CallLogging import io.ktor.features.ContentNegotiation import io.ktor.features.StatusPages import io.ktor.gson.gson import io.ktor.http.HttpStatusCode import io.ktor.response.respond import io.ktor.routing.routing import io.ktor.util.KtorExperimentalAPI import org.jetbrains.exposed.dao.exceptions.EntityNotFoundException import org.kodein.di.ktor.di fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @KtorExperimentalAPI @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { initDB() install(ContentNegotiation) { gson { } } install(CallLogging) install(StatusPages) { exception<EntityNotFoundException> { call.respond(HttpStatusCode.NotFound) } } di { bindServies() } routing { apiRoute() } }

Running the Server

Now we are able to startup our server. The easiest way is to simply click the green arrow in IntelliJ but you can also use ./gradlew run from the command line.

For testing our API I use IntelliJ’s http client feature with following configurations:

// get all books GET http://localhost:8080/api/v1/books Accept: application/json ### // add a book POST http://localhost:8080/api/v1/book Content-Type: application/json { "title": "my new book", "author": "new author" } ### //delete a book DELETE http://localhost:8080/api/v1/book/1 Content-Type: application/json

Anyway, these requests can easily be translated to many other tools like cURL or Postman.

Conclusion

Compared to using Spring Boot, using Ktor, Exposed and Kodein is really simple to setup. Although Spring Boot might have advantages for larger scale backends, for smaller applications Ktor might save you a lot of time. Especially, for prototyping it is a real (JVM-based) alternative to using python with Django.

I hope this tutorial helps you to understand how Ktor, Exposed and Kodein can be used and how an extendable architecture can look like. I’m always happy to receive feedback on my tutorials so let me know what you think in the comments below or by writing me an email.

3 responses to “Tutorial: Simple Kotlin REST API with Ktor, Exposed and Kodein”

  1. juliusham says:

    thank u for the tut,
    in your conclusion u say “Although Spring Boot might have advantages for larger scale backends” whay In your opinion do u think limits ktor to handle larger backends? am considering using ktor for a large highly scalable app?

    • Stefan Galler says:

      I don’t think that performance will be a problem when using Ktor. But I have the feeling, that it might get difficult to structure the project right by extensively using Kotlin extension functions.

  2. Great tutorial, thank you! Ktor, and Kotlin in general, seem so elegant, even on the server. Thanks for covering API versioning too.

    Re: targeting JVM 1.8, while looking at Ktor examples, I saw someone else added this instead:

    “`
    sourceCompatibility = 1.8
    compileKotlin { kotlinOptions.jvmTarget = “1.8” }
    compileTestKotlin { kotlinOptions.jvmTarget = “1.8” }
    “`

    It looks like it equates to the same thing that you have, it’s just more concise.

    Any thoughts on deployments, specifically deploying to Heroku?

    Thanks again!

Leave a Reply

Your email address will not be published. Required fields are marked *