Creating HTTP APIs
In this tutorial, we're going to create an HTTP API that can serve as a backend for any application, be it mobile, web, desktop, or even a B2B service. We will see how routes are defined and structured, how serialization plugins help simplify tedious tasks, and how we can test parts of our application both manually and automated.
Throughout the tutorial, we'll build a simple JSON API that allows us to query information about the customers of our fictitious business, as well as the orders we currently want to fulfill. We will build a convenient way of listing all customers and orders in our system, get information for individual customers and orders, and provide the functionality to add new entries and remove old entries.
Prerequisites
Before starting this tutorial:
Make sure the Ktor plugin is installed and enabled.
Create a new Ktor project
To create a base project for our application using the Ktor plugin, open IntelliJ IDEA and follow the steps below:
On the Welcome screen, click New Project.
Otherwise, from the main menu, select
.In the New Project wizard, choose Ktor from the list on the left. On the right pane, specify the following settings:
Name: Specify a project name.
Location: Specify a directory for your project.
Build System: Make sure that Gradle Kotlin is selected as a build system.
Website: Leave the default
example.com
value as a domain used to generate a package name.Artifact: This field shows a generated artifact name.
Ktor version: Choose the latest Ktor version.
Engine: Leave the default Netty engine.
Configuration in: Choose HOCON file to specify server parameters in a dedicated configuration file.
Add sample code: Disable this option to skip adding sample code for plugins.
Click Next.
On the next page, add the Routing, Content Negotiation, and kotlinx.serialization plugins:
Click Create and wait until IntelliJ IDEA generates a project and installs the dependencies.
Examine the project
To look at the structure of the generated project, let's invoke the Project view:
![Initial project structure Initial project structure](images/tutorial_http_api_project_structure.png)
The
build.gradle.kts
file contains dependencies required for a Ktor server and plugins.The
main/resources
folder includes configuration files.The
main/kotlin
folder contains the generated source code.
Dependencies
First, let's open the build.gradle.kts
file and examine added dependencies:
Let's briefly go through these dependencies one by one:
ktor-server-core
adds Ktor's core components to our project.ktor-server-netty
adds the Netty engine to our project, allowing us to use server functionality without having to rely on an external application container.ktor-server-content-negotiation
andktor-serialization-kotlinx-json
provide a convenient mechanism for converting Kotlin objects into a serialized form like JSON, and vice versa. We will use it to format our APIs output, and to consume user input that is structured in JSON. In order to usektor-serialization-kotlinx-json
, we also have to apply theplugin.serialization
plugin.plugins { id("io.ktor.plugin") version "2.3.5" }logback-classic
provides an implementation of SLF4J, allowing us to see nicely formatted logs in a console.ktor-server-test-host
andkotlin-test-junit
allow us to test parts of our Ktor application without having to use the whole HTTP stack in the process. We will use this to define unit tests for our project.
Configurations: application.conf and logback.xml
The generated project also includes the application.conf
and logback.xml
configuration files located in the resources
folder:
application.conf
is a configuration file in HOCON format. Ktor uses this file to determine the port on which it should run, and it also defines the entry point of our application.ktor { deployment { port = 8080 port = ${?PORT} } application { modules = [ com.example.ApplicationKt.module ] } }If you'd like to learn more about how a Ktor server is configured, check out the Configuration in a file help topic.
logback.xml
sets up the basic logging structure for our server. If you'd like to learn more about logging in Ktor, check out the Logging topic.
Source code
The application.conf file configures the entry point of our application to be com.example.ApplicationKt.module
. This corresponds to the Application.module()
function in Application.kt
, which is an application module:
This module, in turn, calls the following extension functions:
configureRouting
is a function defined inplugins/Routing.kt
, which currently doesn't do anything:fun Application.configureRouting() { routing { } }We'll define the routes for customers and orders in the next chapters.
configureSerialization
is a function defined inplugins/Serialization.kt
, which installsContentNegotiation
and enables thejson
serializer:fun Application.configureSerialization() { install(ContentNegotiation) { json() } }
Customer routes
First, let's tackle the Customer
side of our application. We need to create a model which defines the data that's associated with a customer. We also need to create a series of endpoints to allow Customers to be added, listed, and deleted.
Create the Customer model
For our case, a customer should store some basic information in the form of text: a customer should have an id
by which we can identify them, a first and last name, and an email address. An easy way to model this in Kotlin is by using a data class:
Create a new package named
models
insidecom.example
.Create a
Customer.kt
file in themodels
package and add the following code:package com.example.models import kotlinx.serialization.Serializable @Serializable data class Customer(val id: String, val firstName: String, val lastName: String, val email: String)Note that we are using the
@Serializable
annotation from kotlinx.serialization. Together with its Ktor integration, this will allow us to generate the JSON representation we need for our API responses automatically – as we will see in just a bit.
Create the Customer storage
To not complicate the code, for this tutorial, we'll be using an in-memory storage (i.e. a mutable list of Customer
s) – in a real application, we would be storing this information in a database so that it doesn't get lost after restarting our application. We can add this line right after the data class declaration in Customer.kt
file:
Now that we have a well-defined Customer
class and a storage for our customer objects, it's time we create endpoints and expose them via our API!
Define the routing for customers
We want to respond to GET
, POST
, and DELETE
requests on the /customer
endpoint. As such, let's define our routes with the corresponding HTTP methods.
Create a new package named
routes
insidecom.example
.Create a
CustomerRoutes.kt
file called in theroutes
package, and fill it with the following:package com.example.routes import io.ktor.server.routing.* fun Route.customerRouting() { route("/customer") { get { } get("{id?}") { } post { } delete("{id?}") { } } }In this case, we're using the
route
function to group everything that falls under the/customer
endpoint. We then create a block for each HTTP method. This is just one approach how we can structure our routes – when we tackle theOrder
routes in the next chapter, we will see another approach.Notice also how we actually have two entries for
get
: one without a path parameter, and the other with{id?}
. We'll use the first entry to list all customers, and the second entry to display a specific one.
List all customers
To list all customers, we can return the customerStorage
list by using the call.respond
function in Ktor, which can take a Kotlin object and return it serialized in a specified format. For the get
handler, it looks like this:
In order for this to work, we need the ContentNegotiation
plugin, which is already installed with the json
serializer in plugins/Serialization.kt
. What does content negotiation do? Let us consider the following request:
When a client makes such a request, content negotiation allows the server to examine the Accept
header, see if it can serve this specific type of content, and if so, return the result.
JSON support is powered by kotlinx.serialization. We previously used its annotation @Serializable
to annotate our Customer
data class, meaning that Ktor now knows how to serialize Customer
s (and collections of Customer
s!)
Return a specific customer
Another route we want to support is one that returns a specific customer based on their ID (in this case, 200
):
In Ktor, paths can also contain parameters that match specific path segments. We can access their value using the indexed access operator (call.parameters["myParamName"]
). Let's add the following code to the get("{id?}")
entry:
First, we check whether the parameter id
exists in the request. If it does not exist, we respond with a 400 Bad Request
status code and an error message, and are done. If the parameter exists, we try to find
the corresponding record in our customerStorage
. If we find it, we'll respond with the object. Otherwise, we'll return a 404 "Not Found" status code with an error message.
Create a customer
Next, we implement the option for a client to POST
a JSON representation of a client object, which then gets put into our customer storage. Its implementation looks like this:
call.receive
integrates with the configured Content Negotiation plugin. Calling it with the generic parameter Customer
automatically deserializes the JSON request body into a Kotlin Customer
object. We can then add the customer to our storage and respond with a status code of 201 Created
.
At this point, it is worth highlighting again that in this tutorial, we are also intentionally glancing over issues that could arise from, for example, multiple requests accessing the storage simultaneously. In production, data structures and code that can be accessed from multiple requests/threads at the same time should account for these cases – something that is out of the scope of this tutorial.
Delete a customer
The implementation for deleting a customer follows a similar procedure as we have used for listing a specific customer. We first get the id
and then modify our customerStorage
accordingly:
Similar to the definition of our get
request, we make sure that the id
is not null. If the id
is absent, we respond with a 400 Bad Request
error.
Register the routes
Up until now, we have only defined our routes inside an extension function on Route
– so Ktor doesn't know about our routes yet, and we need to register them. Open the plugins/Routing.kt
file and add the following code:
As you might remember, the configureRouting
function is already invoked in our Application.module()
function in Application.kt
.
We've now completed the implementation for the customer-related routes in our API. If you would like to validate that everything works right away, you can skip ahead to the chapter Test HTTP endpoints manually. If you can still bear the suspense, we can move on to the implementation of order-related routes.
Order routes
Now that we have API endpoints for Customer
s done, let's move on to Orders
. While some of the implementation is rather similar, we will be using a different way of structuring our application routes, and include routes that sum up the price of individual items in an order.
Create the Order model
The orders we want to store in our system should be identifiable by an order number (which might contain dashes), and should contain a list of order items. These order items should have a textual description, the number of how often this item appears in the order, as well as the price for the individual item (so that we can compute the total price of an order on demand).
Inside the models
package, create a new file called Order.kt
and fill it with the definition of the two data classes:
We also once again need a place to store our orders. To skip having to define a POST
route – something you're more than welcome to attempt on your own using the knowledge from the Customer
routes – we will prepopulate our orderStorage
with some sample orders. We can again define it as a top-level declaration inside the Order.kt
file.
Define the routing for orders
We respond to a set of GET
requests with three different patterns:
The first will return all orders, the second will return an order given the id
, and the third will return the total of an order (prices of individual OrderItems
multiplied by number of each item).
With orders, we're going to follow a different pattern when it comes to defining routes. Instead of grouping all routes under a single route
function with different HTTP methods, we'll use individual functions.
List all and individual orders
For listing all orders, we'll follow the same pattern as with customers – the difference being that we're defining it in its own function. Let's create a file called OrderRoutes.kt
inside the routes
package, and start with the implementation of the route inside a function called listOrdersRoute()
.
We apply the same structure to individual orders – with a similar implementation to customers, but encapsulated in its own function:
Totalize an order
Getting the total amount of an order consists of iterating over the items of an order and totalizing this. Implemented as a totalizeOrderRoute
function, it looks like this, which besides the summing process should already look familiar:
A small thing to note here is that we are not limited to suffixes of routes for parameters – as we can see, it's absolutely possible to have a section in the middle be a path parameter (/order/{id}/total
).
Register the routes
Finally, much like the case of customers, we need to register the routes. Hopefully, this makes it clear why grouping routes makes more sense as the number of routes grows. Still in plugins/Routing.kt
, add the routes for orders as follows:
Now that we have everything wired up, we can finally start testing our application, and see if everything works as we would expect it to!
Test HTTP endpoints manually
Now that we have all the endpoints ready, it's time to test our application. While we can use any browser to test GET
requests, we'll need a separate tool to test the other HTTP methods:
If you use IntelliJ IDEA Ultimate, you already have a client that supports
.http
files, allowing you to specify and execute requests.If you use IntelliJ IDEA Community or another IDE, you can test your API using
curl
.
Create a customer HTTP test file
Let's create a new CustomerTest.http
file in the project root:
Inside this file, we have now specified a bunch of HTTP requests using all the supported HTTP methods of our API. IntelliJ IDEA now allows us to run each of these requests individually or all together. To really see what's going on, let's run them individually. But first, we need to make sure our API is actually reachable!
Run API server
Before we can run a request, we need to first start our API server. Open the Application.kt
file and click on the Run icon in the gutter:
![Run Server Run Server](images/run-app.png)
Once the server is up and running, we can execute each request by using the Run icon in the gutter:
![Run POST Request Run POST Request](images/run-post-request.png)
If everything is correct, we should see the output in the Run tool window:
![Run Output Run Output](images/run-output.png)
Order endpoints
For the order endpoints, we can follow the same procedure: we create a new file called OrderTest.http
in the root of our project and fill it with some HTTP requests:
Running these requests just as the ones before, we should see the expected output – list of orders, information about one order, and the total of the order.
Automated testing
While manual testing is great and necessary, it also makes sense to have automated testing of endpoints. Ktor allows us to test endpoints without having to start up the entire underlying engine (such as Netty) using the testApplication
function. Inside this function, you need to use the existing Ktor client instance to make requests to your server and verify the results.
Let's write a unit test to ensure that our order route returns properly formatted JSON content. Open the src/test/kotlin/com/example/ApplicationTest.kt
file and replace the existing test class with OrderRouteTests
:
Note that since our string contains a lot of quotation marks around keys and values (like "number"
), this is a great place to use raw strings using triple-quotes ("""
), saving us the hassle of individually escaping every special character inside the string.
We can now run our unit test from the IDE and see the results. Much like we've done for this endpoint, we can add all other endpoints as tests and automate the testing of our HTTP API.
And just like that, we have finished building our small JSON-based HTTP API. Of course, there are tons of topics you can still explore around Ktor and building APIs with it, so your learning journey doesn't have to stop here!
What's next
With this step, we've finalized our HTTP API application. From here on, we can add other plugins such as Authentication, etc.
Feature requests
Authentication: currently, the API is open to whomever would like to access it. If you want to restrict access, have a look at Ktor's support for JWT and other authentication methods.
Learn more about route organization! If you'd like to learn about different ways to organize your routes with Ktor, check out the Routing topic.
Persistence! Currently, all customers and orders vanish when we stop our application, as we are only storing them in an in-memory storage. You could try integrating your application with a database like PostgreSQL or MongoDB, using one of the plenty projects that allow database access from Kotlin, like Exposed or KMongo.
Integrate with a client! Now that we are exposing data, it would make sense to explore how this data can be consumed again! Try writing an API client using the Ktor HTTP client, for example, or try accessing it from a website using JavaScript or Kotlin/JS!
To make sure your API works nicely with browser clients, you should also set up a policy for Cross-Origin Resource Sharing (CORS). The simplest and most permissive way to do this with Ktor would be by adding the following snippet to the top of
Application.module()
:install(CORS) { anyHost() }