πŸ“š Backend development

Many frameworks officially support Kotlin: Springopen in new window, Quarkusopen in new window, Ktoropen in new window, among others listed hereopen in new window.

In addition to that, Kotlin is theoretically compatible with any framework that targets the JVM or JS. For example, this tutorial shows how to use nodejs with Kotlinopen in new window. However, frameworks that do not officially support Kotlin may require some tweaking to use it.

Ktor

Ktor is a cross-platform Kotlin library for building both HTTP clients and servers. This makes Ktor a useful library to learn for both front-end developers for its HTTP client capabilities and backend-development for its HTTP server capabilities. In the following, we'll create a REST API with Ktor server.

πŸ§ͺ develop an API with Ktor

  • Create a project on start.ktor.ioopen in new window with the following plugins: Content Negotiation, kotlinx.serialization, and Routing.
  • Click on "Generate project".
  • Download the archive, unzip it, and open the project with IntelliJ.
  • Create a models package and add to it a Customer data class with these immutable properties id: String, firstName: String, lastName: String, email: String.
  • Annotate the class with @Serializable.
  • Create a new package named routes and add to it a file CustomerRoutes.kt that will contain the code for the /customer endpoint.
  • The code below provides the implementation of some endpoints. Please implement the remaining ones.
  • To enable the route call customerRouting() in the routing configuration file located in plugins/Routing.kt.
  • For simplicity, use a global in-memory list of customers val store = mutableListOf<Customer>().
  • Run the server by running the main method.
  • Test the api on the IDE by using an http file or using any other client.
CustomerRoutes.kt
val store = mutableListOf<Customer>()

fun Route.customerRouting() {
    route("/customer") {
        get {
            call.respond(store)
        }
        get("{id?}") {
            val id = call.parameters["id"] ?: return@get call.respondText(
                "Missing id",
                status = HttpStatusCode.BadRequest
            )
            val customer =
                store.find { it.id == id } ?: return@get call.respondText(
                    "No customer with id $id",
                    status = HttpStatusCode.NotFound
                )
            call.respond(customer)
        }
        post {
            val customer = call.receive<Customer>()
            store.add(customer)
            call.respondText("Customer stored correctly", status = HttpStatusCode.Created)
        }
        delete("{id?}") {

        }
    }
}
plugins/Routing.kt
fun Application.configureRouting() {
    routing {
        customerRouting()
    }
}

return@label

You can specify which level you want to return with an explicit label using return@lambda.

lambdaA {
    lambdaB {
        lambdaC {
            val randomInt = Random.nextInt(0, 100)
            if (randomInt > 50) return@lambdaC else return@lambdaB
        }
        printf("In lambdaB")
    }
}

This code runs another exampleopen in new window.

CustomerTest.http
POST http://127.0.0.1:8080/customer
Content-Type: application/json

{
  "id": "100",
  "firstName": "Jane",
  "lastName": "Smith",
  "email": "jane.smith@company.com"
}


###
POST http://127.0.0.1:8080/customer
Content-Type: application/json

{
  "id": "200",
  "firstName": "John",
  "lastName": "Smith",
  "email": "john.smith@company.com"
}

###
POST http://127.0.0.1:8080/customer
Content-Type: application/json

{
  "id": "300",
  "firstName": "Mary",
  "lastName": "Smith",
  "email": "mary.smith@company.com"
}


###
GET http://127.0.0.1:8080/customer
Accept: application/json

###
GET http://127.0.0.1:8080/customer/200
Accept: application/json

###
GET http://127.0.0.1:8080/customer/500
Accept: application/json

###
DELETE http://127.0.0.1:8080/customer/100

###
DELETE http://127.0.0.1:8080/customer/500

This page has detailed stepsopen in new window

Spring framework

Spring is a famous framework for developing server-side applications: APIs, server generated web pages, microservices, etc. It relies on the the Java ecosystem to build and run, thus making it compatible with Kotlin. Even better, Spring officially supports Kotlin. It even allows in start a new project with Kotlin and Gradle-Kotlin. In the next section, we'll use this starter to recreate our above REST API with Spring.

πŸ§ͺ Spring boot part 1 - develop the same API with Spring Boot

  • Create a project on start.spring.io (also called Spring initializr)open in new window with the following dependencies: Spring Web and Spring Boot DevTools.
  • Choose Kotlin as the language and Kotlin-Grade as the project manager.
  • Add these dependencies: Spring Web, Spring Boot DevTools, h2 database and Spring Data JPA.
  • Click on "Generate". Download the archive, unzip it, and open the project with IntelliJ (preferably) or VSCode.
  • Create Customer data class in the model package without the @Serializable annotation.
  • Create a controller package that contains a CustomerController class which provides a CRUD using a global list. You can find a skeleton below.
    • πŸ’‘ In Spring, Rest controllers serve the purpose of Ktor routes, where a controller defines a REST resource.
  • Define the same endpoints as in the previous PW.
  • Start the REST API server by running .\gradlew bootRun or from your IDE.
  • Please test the endpoints with a REST client. You can find http files here in JetBrains formatopen in new window or VSCode's REST Client extensionopen in new window
CustomerController.kt
val store = mutableListOf<Customer>()

@RestController
@RequestMapping("/customer")
class CustomerController {
    @GetMapping
    fun getAll() = store

    @GetMapping("{id}")
    fun getById(@PathVariable id: String) { /* TODO: implement */ }

    @PostMapping
    fun addOne(@RequestBody customer: Customer) { /* TODO: implement */ }

    @DeleteMapping("{id}")
    fun deleteOne(@PathVariable id: String) { /* TODO: implement */ }
}

Models or model package ? plural or not ?

Both are ok as long as you follow the same convention in the project.

πŸ§ͺ Spring boot part 2 - adding a database

Let's go a little bit further by storing data in a database and writing some tests.

We'll use the H2 in-memory database for the sake of simplicity, since it does not require a server to run. Classes will mapped to database tables with JPA annotations. The database API we'll be using is called JPARepository. It is a lightweight API that provides common CRUD features by just defining an interface.

On the testing side, we'll see two different syntaxes. The default one that is more familiar with Java style and the DSL one which is more readable and more familiar with Kotlin developers.

  • Create a new Spring project using Spring initializropen in new window with Kotlin and the following dependencies: Spring Data JPA, H2 Database, Spring Boot DevTools, Spring Web
  • Open the project and add this class in the model package @Entity class Product(@Id @GeneratedValue var id: Long? = null, var name: String, var price: Int). This single defines the class as well as the minimal JPA annotations (@Entity, @Id and @GeneratedValue) to generate the corresponding table.
  • In the repository package, declare the ProductRepository interface as follows interface ProductRepository: JpaRepository<Product, Long>. This is enough for Spring to generate an implementation with common features as we'll see later.
  • Next, create a ProductService class which will contain the business logic. In terms of architecture, the controller calls a service which in turn rely on other services or repositories.
ProductService.kt
@Service
class ProductService(@Autowired val productRepository: ProductRepository) {
    fun getAll() = productRepository.findAll()

    // use findByIdOrNull instad of findById because the latter returns an optional<Product> instead of Product?
    fun getById(id: Long) = productRepository.findByIdOrNull(id)
}
  • In the controller package, create a ProductController class that is mapped to /product and injects the with @Autowired. Reply to @Get as follows.
ProductController.kt
@RestController
@RequestMapping("/product")
class ProductController(@Autowired val productService: ProductService) {
    @GetMapping fun getAll() = productService.getAll()

    @GetMapping("{id}")
    fun getById(@PathVariable id: Long) =
        productService.getById(id) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}

Kotlin makes getById(@PathVariable id: Long) more concise

The Elvis operator ?: allows to simplify the code. Here is a longer version as reference.

@GetMapping("{id}")
fun getById(@PathVariable id: Long): Product {
    val product = productService.getById(id)
    if (product != null){
        return product
    }
    throw ResponseStatusException(HttpStatus.NOT_FOUND)
}

In addition to that, Spring provides @ControllerAdvice to change the exception message. You can see an example hereopen in new window.

  • Let's run the project. Before running the project, we need to add a plugin that allows Kotlin classes to generate a default constructor id("org.jetbrains.kotlin.plugin.jpa") version "1.8.10". The plugins should look as follows:
plugins {
    id("org.jetbrains.kotlin.plugin.jpa") version "1.8.10"
	id("org.springframework.boot") version "3.0.4"
	id("io.spring.dependency-management") version "1.1.0"
	kotlin("jvm") version "1.8.10"
	kotlin("plugin.spring") version "1.8.10"
}
  • As an exercise, implement these endpoints: POST a single product, DELETE by id (/product/{id}) and GET by id (/product/{id}).
    • Hint: ProductController already provides the necessary methods.
  • Call the different endpoints with a REST client.

πŸ§ͺ Spring boot part 2 - adding tests

Spring frameworks helps perform different types of tests by providing different classes out of the box:

  • Unit testing of services, repositories and the REST API. This is done through mock utilities such as MockMVC.
  • Integration testing of the REST API using TestRestTemplate. In this situation, a full server is run and tested.

Most, if not all classes provided by Spring provide an elegant syntax for Java developers. Some of them go further by taking advantage of Kotlin specific features. In the following, we're going to focus on parts that provide Kotlin DSLs, namely unit testing the REST API with MockMVC.

  • Create a test class ProductControllerUnitTests with this initial content. MockMvc allows to unit test the REST API. The @AutoConfigureMockMvc annotation allows spring to configure it automatically
@SpringBootTest
@AutoConfigureMockMvc
class ProductControllerTests(
    @Autowired val mockMvc: MockMvc,
    @Autowired val productRepository: ProductRepository) {

    @BeforeEach
    fun reset(){
        productRepository.deleteAll()
    }
}
  • Add these two tests. The first one uses a classic approach while the second take advantage of Kotlin DSL capabilities. In addition to that, we name using a more readable string literal
@Test
fun testWithClassicApproach(){
    mockMvc.perform(get("/product"))
        .andExpect(status().isOk)
        .andExpect(content().string(containsString("[]")))
}
@Test
fun `test GET a single product`() {
    mockMvc.get("/product/1").andExpect {
        status { isOk() }
        jsonPath("$.name") { value("A") }
        jsonPath("$.price") { value(1) }
        content { contentType(MediaType.APPLICATION_JSON) }
    }
}
  • As an exercise, unit tests for the other endpoints.

The request builder of JpaRepository

Spring repositories implement requests based on the name of their methods. For example, to get all products sorted by name, we can add this method to the interface.

interface ProductRepository: JpaRepository<Product, Long> {
    fun findAllByOrderByNameAsc(): List<Product>;
}

The official documentationopen in new window provides more detailed explanations and examples.

NodeJS

Thanks to Kotlin/JS, we can write apps that target nodejs using Kotlin. We can even import npm libraries as long as you declare the JS API surface that you'll be using in Kotlin. This is called external declaration (You can think of it as an equivalent of TypeScript's type definitions) that declares the symbols that we want to access in Kotlin thanks to @JsModuleopen in new window and @JsNonModuleopen in new window annotations.

Defining such external declarations can be a hassle and there seems to be no official automatic generator (dukatopen in new window has been removed in kotlin 1.8.20). In that case, we have two options, either we write the external declaration ourselves or import it as a dependency if available. Fortunately for express developers, chrisnkrueger/kotlin-expressopen in new window provides declarations for the express library.

There are two gradle plugins that allow to create nodeJs projects: the kotlin("js") one and the kotlin("multiplatform") one. The difference between the two plugins is that the former only supports JS or WASM while the latter supports more platforms but requires to configure source sets. Thus, the former may seem easier to setup but the latter is better in the long run because it allows us to get more familiar with Kotlin Multiplatform (KMP).

πŸ§ͺ Getting started with Kotlin/JS and Express

At the time of writing, I didn't find an official wizard or starter project. So we'll create one from scratch using gradle init.

  • Create a new Gradle project using IntelliJ or by running gradle init in a empty folder (see below for the replies to the gradle init command).
gradle init
gradle init
Starting a Gradle Daemon, 1 incompatible and 1 stopped Daemons could not be reused, use --status for details

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 1

Select build script DSL:
  1: Kotlin
  2: Groovy
Enter selection (default: Kotlin) [1..2] 1

Project name (default: starter): rest-api-kotlin-nodejs

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] yes


> Task :init
To learn more about Gradle by exploring our Samples at https://docs.gradle.org/8.3/samples

BUILD SUCCESSFUL in 24s
2 actionable tasks: 2 executed
  • In build.gradle.kts, add and configure the kotlin("multiplatform") plugin. Also add the express and dev.chriskrueger:kotlin-express dependencies.
build.gradle.kts
plugins {
    kotlin("multiplatform") version "1.9.20-Beta"
}

repositories {
    mavenCentral()
}

group = "tech.worldline.demo"
version = "1.0-SNAPSHOT"

kotlin {
    js {
        nodejs {
        }
        binaries.executable()
        useCommonJs()
    }

    sourceSets {
        val jsMain by getting {
            dependencies {
                implementation(npm("express", "> 4.0.0 < 5.0.0"))
                implementation("dev.chriskrueger:kotlin-express:1.2.0")
            }
        }
    }
}

Some notes on the build file

  • create a main.kt file in src/jsMain/kotlin with the following content:
main.kt
data class Message(val id: Int, val message: String)

val messages = mutableListOf(Message(0, "I love Kotlin/JS"))

fun main() {
    val app = express.Express()

    // REST API that provides a **GET /hello** endpoint
    app.get("/hello") { _, res ->
        res.send(messages)
    }

    // Create a server that listens to port 3000
    app.listen(3000) {
        console.log("server start at port 3000")
    }
}

Execution failed for task ':kotlinStoreYarnLock'

If you get this error:

Execution failed for task ':kotlinStoreYarnLock'.
> yarn.lock was changed. Run the `kotlinUpgradeYarnLock` task to actualize yarn.lock file

Run ./gradlew kotlinUpgradeYarnLock so that yarn.lock is updated

πŸ§ͺ Adding a post endpoint and an external Kotlin/JS definition

Let's add a post endpoint which reads the body as a json. In order to read the body as json, we must add this possibility to express by importing the npm library body-parseropen in new window and by calling app.use(bodyParser.json()). Once this setup is complete, req.body will contain the content of the body. However, there is no available external definition for bodyParser as of the time of writing. Thus, we must create or own external definition.

  • First, add the body-parser dependncy in the build file implementation(npm("body-parser", "> 1.0.0 < 2.0.0"))
  • Next, we would write: app.use(bodyparser.json()) to activate the library. Let's guess what a minimal definition of bodyparser can be.
BodyParser.kt
// external means that this class is defined in JS
external class BodyParser {
    // we tell Kotlin that we want to use the json() function.
    fun json(): Any
    // It is not required to define all the functions of the module
}

// @JsModule is used to import the module from the NPM registry
@JsModule("body-parser")
external val bodyParser: BodyParser
  • Finally, we just need to add the BodyParser.kt file into the project and use it in our server.
main.kt
app.use(bodyParser.json())
app.post("/hello") { req, res ->
    // Kotlin does not keep the original field name when parsing JSON from JS (you can see it the in get response)
    if (req.body as? Message == null) {
        println("failed to get the body from Kotlin")
    }
    // Thus, we need to use js() to get the the field by its expected name
    // js() calls JS from Kotlin
    println("req.body from JS ${js("req.body.id")} - ${js("req.body.message")}")
    val id = js("req.body.id") as? Int
    val message = js("req.body.message") as? String
    if (message != null && id != null) {
        messages.add(Message(id, message))
        res.status(201).end()
    } else {
        res.status(400).send(js("{cause : 'error'}") as Any)
    }
}

πŸ§ͺ Adding more endpoints

  • Add PUT and DELETE endpoints

🎯 Solutions

πŸ“– Further readings

These official tutorials go even further:

References