Ktor 2.3.12 Help

How to create RESTful APIs in Kotlin with Ktor

In this tutorial, we’ll explain how to build a backend service using Kotlin and Ktor, featuring an example of a RESTful API that generates JSON files.

In the previous tutorial, we introduced you to the fundamentals of validation, error handling, and unit testing. This tutorial will expand on these topics by creating a RESTful service for managing tasks.

You will learn how to do the following:

  • Create RESTful services that use JSON serialization.

  • Understand the process of Content Negotiation.

  • Define the routes for a REST API within Ktor.

Prerequisites

You can do this tutorial independently, however, we strongly recommend that you complete the preceding tutorial to learn how to handle requests and generate responses.

We recommend that you install IntelliJ IDEA, but you could use another IDE of your choice.

Hello RESTful Task Manager

In this tutorial, you will be rewriting your existing Task Manager as a RESTful service. To do this you will use several Ktor plugins.

While you could manually add it to your existing project, it’s simpler to generate a new project and then incrementally add the code from the previous tutorial. You will reiterate all the code as you go, so you don’t need to have the previous project to hand.

Create a new project with plugins

  1. Navigate to the Ktor Project Generator.

  2. In the Project artifact field, enter com.example.ktor-rest-task-app as the name of your project artifact.

    Naming the project artifact in the Ktor Project Generator

  3. In the plugins section search for and add the following plugins by clicking on the Add button:

    • Routing

    • Content Negotiation

    • Kotlinx.serialization

    • Static Content

    Adding plugins in the Ktor Project Generator
    Once you have added the plugins, you will see all four plugins listed below the project settings.
    Plugins list in the Ktor Project Generator

  4. Click the Download button to generate and download your Ktor project.

Add starter code

  1. Open your project in IntelliJ IDEA, as previously described in the Create, open and run a Ktor project tutorial.

  2. Navigate to src/main/kotlin/com/example and create a subpackage called model.

  3. Inside the model package, create a new Task.kt file.

  4. Open the Task.kt file and add an enum to represent priorities and a class to represent tasks:

    package com.example.model import kotlinx.serialization.Serializable enum class Priority { Low, Medium, High, Vital } @Serializable data class Task( val name: String, val description: String, val priority: Priority )

    In the previous tutorial you used extension functions to convert a Task into HTML. In this case, the Task class is annotated with the Serializable type from the kotlinx.serialization library.

  5. Open the Routing.kt file and replace the existing code with the implementation below:

    package com.example.plugins import com.example.model.* import io.ktor.server.application.* import io.ktor.server.http.content.* import io.ktor.server.response.* import io.ktor.server.routing.* fun Application.configureRouting() { routing { staticResources("static", "static") get("/tasks") { call.respond( listOf( Task("cleaning", "Clean the house", Priority.Low), Task("gardening", "Mow the lawn", Priority.Medium), Task("shopping", "Buy the groceries", Priority.High), Task("painting", "Paint the fence", Priority.Medium) ) ) } } }

    Similar to the previous tutorial, you’ve created a route for GET requests to the URL /tasks. This time, instead of manually converting the list of tasks, you are simply returning the list.

  6. In IntelliJ IDEA, click on the run button (intelliJ IDEA run icon) to start the application.

  7. Navigate to http://0.0.0.0:8080/tasks in your browser. You should see a JSON version of the list of tasks, as shown below:

JSON data displayed in a browser screen

Clearly a lot of work is being performed on our behalf. What exactly is going on?

Understand Content Negotiation

Content Negotiation via the browser

When you created the project you included the Content Negotiation plugin. This plugin looks at the types of content that the client can render and matches these against the content types that the current service can provide. Hence, the term Content Negotiation.

In HTTP the client signals which content types it can render through the Accept header. The value of this header is one or more content types. In the case above you can examine the value of this header by using the development tools built into your browser.

Consider the following example:

text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

Note the inclusion of */* .This header signals that it accepts HTML, XML or Images - but it would also accept any other content type.

The Content Negotiation plugin needs to find a format to send data back to the browser. If you look inside the generated code in the project you will find a file called Serialization.kt inside src/kotlin/main/com/example/plugins, which includes the following:

install(ContentNegotiation) { json() }

This code installs the ContentNegotiation plugin, and also configures the kotlinx.serialization plugin. With this, when clients send requests the server can send back objects serialized as JSON.

In the case of the request from the browser the ContentNegotiation plugin knows it can only return JSON, and the browser will try to display anything it is sent. So the request succeeds.

Content Negotiation via JavaScript

In a production environment, you wouldn’t want to display JSON directly in the browser. Instead, there would be JavaScript code running within the browser, which would make the request and then display the data returned as part of a Single Page Application (SPA). Typically, this kind of application would be written using a framework like React, Angular, or Vue.js.

  1. To simulate this, open the index.html page inside src/main/resources/static and replace the default content with the following:

    <html> <head> <title>A Simple SPA For Tasks</title> <script type="application/javascript"> function fetchAndDisplayTasks() { fetchTasks() .then(tasks => displayTasks(tasks)) } function fetchTasks() { return fetch( "/tasks", { headers: { 'Accept': 'application/json' } } ).then(resp => resp.json()); } function displayTasks(tasks) { const tasksTableBody = document.getElementById("tasksTableBody") tasks.forEach(task => { const newRow = taskRow(task); tasksTableBody.appendChild(newRow); }); } function taskRow(task) { return tr([ td(task.name), td(task.description), td(task.priority) ]); } function tr(children) { const node = document.createElement("tr"); children.forEach(child => node.appendChild(child)); return node; } function td(text) { const node = document.createElement("td"); node.appendChild(document.createTextNode(text)); return node; } </script> </head> <body> <h1>Viewing Tasks Via JS</h1> <form action="javascript:fetchAndDisplayTasks()"> <input type="submit" value="View The Tasks"> </form> <table> <thead> <tr><th>Name</th><th>Description</th><th>Priority</th></tr> </thead> <tbody id="tasksTableBody"> </tbody> </table> </body> </html>

    This page contains an HTML form and an empty table. Upon submitting the form a JavaScript event handler sends a request to the /tasks endpoint, with the Accept header set to application/json. The data returned is then de-serialized and added to an HTML table.

  2. In IntelliJ IDEA, click the rerun button (intelliJ IDEA rerun icon) to restart the application.

  3. Navigate to the URL http://0.0.0.0:8080/static/index.html. You should be able to fetch the data by clicking on the View The Tasks button:

    A browser window showing a button and tasks displayed as an HTML table

Add the GET routes

Now that you are familiar with the process of content negotiation, continue with transferring the functionality from the previous tutorial into this one.

Reuse the Task Repository

You can reuse the repository for Tasks without any modification, so let’s do that first.

  1. Inside the model package create a new TaskRepository.kt file.

  2. Open TaskRepository.kt and add the code below:

    package com.example.model object TaskRepository { private val tasks = mutableListOf( Task("cleaning", "Clean the house", Priority.Low), Task("gardening", "Mow the lawn", Priority.Medium), Task("shopping", "Buy the groceries", Priority.High), Task("painting", "Paint the fence", Priority.Medium) ) fun allTasks(): List<Task> = tasks fun tasksByPriority(priority: Priority) = tasks.filter { it.priority == priority } fun taskByName(name: String) = tasks.find { it.name.equals(name, ignoreCase = true) } fun addTask(task: Task) { if (taskByName(task.name) != null) { throw IllegalStateException("Cannot duplicate task names!") } tasks.add(task) } }

Reuse the routes for GET requests

Now that you’ve created the repository, you can implement the routes for GET requests. The previous code can be simplified because you no longer need to worry about converting tasks to HTML:

  1. Navigate to the Routing.kt file in src/main/kotlin/com/example/plugins.

  2. Update the code for the /tasks route inside the Application.configureRouting() function with the following implementation:

    package example.com.plugins import com.example.model.Priority import com.example.model.TaskRepository import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.http.content.* import io.ktor.server.response.* import io.ktor.server.routing.* fun Application.configureRouting() { routing { staticResources("static", "static") //updated implementation route("/tasks") { get { val tasks = TaskRepository.allTasks() call.respond(tasks) } get("/byName/{taskName}") { val name = call.parameters["taskName"] if (name == null) { call.respond(HttpStatusCode.BadRequest) return@get } val task = TaskRepository.taskByName(name) if (task == null) { call.respond(HttpStatusCode.NotFound) return@get } call.respond(task) } get("/byPriority/{priority}") { val priorityAsText = call.parameters["priority"] if (priorityAsText == null) { call.respond(HttpStatusCode.BadRequest) return@get } try { val priority = Priority.valueOf(priorityAsText) val tasks = TaskRepository.tasksByPriority(priority) if (tasks.isEmpty()) { call.respond(HttpStatusCode.NotFound) return@get } call.respond(tasks) } catch (ex: IllegalArgumentException) { call.respond(HttpStatusCode.BadRequest) } } } } }

    With this, your server can respond to the following GET requests:

    • /tasks returns all tasks in the repository.

    • /tasks/byName/{taskName} returns tasks filtered by the specified taskName.

    • /tasks/byPriority/{priority} returns tasks filtered by the specified priority.

  3. In IntelliJ IDEA, click the rerun button (intelliJ IDEA rerun icon) to restart the application.

Test the functionality

Use the browser

You could test these routes in the browser. For example, navigate to http://0.0.0.0:8080/tasks/byPriority/Medium to see all the tasks with a Medium priority displayed in JSON format:

A browser window showing tasks with medium priority in JSON format

Given that these kinds of requests will typically be coming from JavaScript, more fine-grained testing is preferable. For this, you can use a specialist tool such as Postman.

    Use Postman

    1. In Postman, create a new GET request with the URL http://0.0.0.0:8080/tasks/byPriority/Medium.

    2. In the Headers pane, set the value of the Accept header to application/json.

    3. Click Send to send the request and see the response in the response viewer.

      A GET request in Postman showing tasks with medium priority in JSON format

    Use an HTTP Request File

    Within IntelliJ IDEA Ultimate you could perform the same steps in an HTTP Request file.

    1. In the project root directory, create a new REST Task Manager.http file.

    2. Open the REST Task Manager.http file and add the following GET request:

      GET http://0.0.0.0:8080/tasks/byPriority/Medium Accept: application/json
    3. To send the request within IntelliJ IDE, click on the gutter icon (intelliJ IDEA gutter icon) next to it.

    4. This will open and run in the Services tool window:

      A GET request in an HTTP file showing tasks with medium priority in JSON format

    Add a route for POST requests

    In the previous tutorial, tasks were created through an HTML form. However, as you are now building a RESTful service, you no longer need to do that. Instead, you will make use of the kotlinx.serialization framework which will do the majority of the heavy lifting.

    1. Open the Routing.kt file inside src/main/kotlin/com/example/plugins.

    2. Add a new POST route to the Application.configureRouting() function as follows:

      //... fun Application.configureRouting() { routing { //... route("/tasks") { //... //add the following new route post { try { val task = call.receive<Task>() TaskRepository.addTask(task) call.respond(HttpStatusCode.NoContent) } catch (ex: IllegalStateException) { call.respond(HttpStatusCode.BadRequest) } catch (ex: JsonConvertException) { call.respond(HttpStatusCode.BadRequest) } } } } }

      Add the following new imports:

      //... import com.example.model.Task import io.ktor.serialization.* import io.ktor.server.request.*

      When a POST request is sent to /tasks the kotlinx.serialization framework is used to convert the body of the request into a Task object. If this succeeds, the task will be added to the repository. If the deserialization process fails the server will need to handle a JsonConvertException, whereas if the task is a duplicate it will need to handle an IllegalStateException.

    3. Restart the application.

    4. To test this functionality in Postman, create a new POST request to the URL http://0.0.0.0:8080/tasks.

    5. In the Body pane add the following JSON document to represent a new task:

      { "name": "cooking", "description": "Cook the dinner", "priority": "High" }
      A POST request in Postman for adding a new task
    6. Click Send to send the request.

    7. You can verify the task has been added by sending a GET request to http://0.0.0.0:8080/tasks.

    8. Within IntelliJ IDEA Ultimate you could perform the same steps by adding the following to your HTTP Request file:

      ### POST http://0.0.0.0:8080/tasks Content-Type: application/json { "name": "cooking", "description": "Cook the dinner", "priority": "High" }

    Add support for removals

    You have almost finished adding the basic operations to your service. These are often summarized as the CRUD operations - short for Create, Read, Update, and Delete. The only operation you are missing is the Delete.

    1. In the TaskRepository.kt file add the following method within the TaskRepository object to remove tasks based on their name:

      fun removeTask(name: String): Boolean { return tasks.removeIf { it.name == name } }
    2. Open the Routing.kt file and add an endpoint into the routing() function to handle DELETE requests:

      fun Application.configureRouting() { //... routing { route("/tasks") { //... //add the following function delete("/{taskName}") { val name = call.parameters["taskName"] if (name == null) { call.respond(HttpStatusCode.BadRequest) return@delete } if (TaskRepository.removeTask(name)) { call.respond(HttpStatusCode.NoContent) } else { call.respond(HttpStatusCode.NotFound) } } } } }
    3. Restart the application.

    4. Add the following DELETE request to your HTTP Request File:

      ### DELETE http://0.0.0.0:8080/tasks/gardening
    5. To send the DELETE request within IntelliJ IDE, click on the gutter icon (intelliJ IDEA gutter icon) next to it.

    6. You will see the response in the Services tool window:

      A DELETE request in an HTTP Request file

    Create unit tests with Ktor Client

    So far you have tested your application manually, but, as you’ve already noticed, this approach is time-consuming and will not scale. Instead, you can implement JUnit tests, using the built-in client object to fetch and deserialize JSON.

    1. Open the ApplicationTest.kt file within src/test/kotlin/com/example.

    2. Replace the contents of the ApplicationTest.kt file with the following:

      package com.example import com.example.model.Priority import com.example.model.Task import io.ktor.client.call.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationTest { @Test fun tasksCanBeFoundByPriority() = testApplication { val client = createClient { install(ContentNegotiation) { json() } } val response = client.get("/tasks/byPriority/Medium") val results = response.body<List<Task>>() assertEquals(HttpStatusCode.OK, response.status) val expectedTaskNames = listOf("gardening", "painting") val actualTaskNames = results.map(Task::name) assertContentEquals(expectedTaskNames, actualTaskNames) } @Test fun invalidPriorityProduces400() = testApplication { val response = client.get("/tasks/byPriority/Invalid") assertEquals(HttpStatusCode.BadRequest, response.status) } @Test fun unusedPriorityProduces404() = testApplication { val response = client.get("/tasks/byPriority/Vital") assertEquals(HttpStatusCode.NotFound, response.status) } @Test fun newTasksCanBeAdded() = testApplication { val client = createClient { install(ContentNegotiation) { json() } } val task = Task("swimming", "Go to the beach", Priority.Low) val response1 = client.post("/tasks") { header( HttpHeaders.ContentType, ContentType.Application.Json ) setBody(task) } assertEquals(HttpStatusCode.NoContent, response1.status) val response2 = client.get("/tasks") assertEquals(HttpStatusCode.OK, response2.status) val taskNames = response2 .body<List<Task>>() .map { it.name } assertContains(taskNames, "swimming") } }

      Note that you need to install the ContentNegotiation and kotlinx.serialization plugins into the Ktor client, in the same way as you did on the server.

    3. Add the following dependencies into your build.gradle.kts file:

      val ktor_version: String by project //... testImplementation("io.ktor:ktor-server-test-host:$ktor_version") testImplementation("io.ktor:ktor-client-content-negotiation:$ktor_version")

    Create unit tests with JsonPath

    Testing your service with the Ktor client, or a similar library, is convenient, but it has a drawback from a Quality Assurance (QA) perspective. The server, not directly handling JSON, can't be certain about its assumptions regarding the JSON structure.

    For example, assumptions like:

    • Values are being stored in an array when in reality an object is used.

    • Properties are being stored as numbers, when they are in fact strings.

    • Members are being serialized in the order of declaration when they are not.

    If your service is intended for use by multiple clients, it's crucial to have confidence in the JSON structure. To achieve this, use the Ktor Client to retrieve text from the server and then analyze this content using the JSONPath library.

    1. In your build.gradle.kts file, add the JSONPath library to the dependencies block:

      testImplementation("com.jayway.jsonpath:json-path:2.9.0")
    2. Navigate to the src/test/kotlin/com/example folder and create a new ApplicationJsonPathTest.kt file.

    3. Open the ApplicationJsonPathTest.kt file and add the following content to it:

      package com.example import com.jayway.jsonpath.DocumentContext import com.jayway.jsonpath.JsonPath import io.ktor.client.* import com.example.model.Priority import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import kotlin.test.* class ApplicationJsonPathTest { @Test fun tasksCanBeFound() = testApplication { val jsonDoc = client.getAsJsonPath("/tasks") val result: List<String> = jsonDoc.read("$[*].name") assertEquals("cleaning", result[0]) assertEquals("gardening", result[1]) assertEquals("shopping", result[2]) } @Test fun tasksCanBeFoundByPriority() = testApplication { val priority = Priority.Medium val jsonDoc = client.getAsJsonPath("/tasks/byPriority/$priority") val result: List<String> = jsonDoc.read("$[?(@.priority == '$priority')].name") assertEquals(2, result.size) assertEquals("gardening", result[0]) assertEquals("painting", result[1]) } suspend fun HttpClient.getAsJsonPath(url: String): DocumentContext { val response = this.get(url) { accept(ContentType.Application.Json) } return JsonPath.parse(response.bodyAsText()) } }

      The JsonPath queries work as follows:

      • $[*].name means “treat the document as an array and return the value of the name property of each entry”.

      • $[?(@.priority == '$priority')].name means “return the value of the name property of every entry in the array with a priority equal to the supplied value”.

      You can use queries like these to confirm your understanding of the returned JSON. When you do code refactoring and service redeployment, any modifications in serialization will be identified, even if they don't disrupt deserialization with the current framework. This allows you to republish publicly available APIs with confidence.

    Next steps

    Congratulations! You have now completed creating a RESTful API service for your Task Manager application and learned the nits and grits of unit testing with the Ktor Client and JsonPath.

    Continue to the next tutorial to learn how to reuse your API service to build a web application.

    Last modified: 11 July 2024