Getting started with Ktor — Part I: The Basics.
What is Ktor?
At first glance, Ktor (pronounced Kay-tor) seems like another asynchronous framework for web applications, and it kinda is. If you have any experience with frameworks like Express.js or Gin, you’ll be at home with Ktor’s declarative nature. Ktor is developed purely in Kotlin, by the team behind the language itself. Did I also mention it functions great as an HTTP client? But that is a topic for a future post, for now, let’s stick with the server-side of things.
Requirements
You can use your favorite IDE for the job. I like to use IntelliJ Community Edition because it works perfectly with Kotlin. No need for the ultimate edition here, if you own the license though, you can use the Ktor plugin for some extra features.
As for runtime, I’d recommend using GraalVM 11, it boots everything quickly and has a very low memory footprint. Everything with OpenJDK so no license problems here either.
Well, now that you have everything set up and ready to run, let’s get started.
Using Ktor starter
The easiest way to create a project with Ktor is by heading to https://start.ktor.io/#. This is the project generator for Ktor, where we will set up most of our initial features.
First, let’s talk about the settings available on the left-most panel.
We’ll be using Gradle as a build tool with the Kotlin DSL which makes it very neat and readable, if you prefer using Gradle as is, or even using Maven, you can choose it with the first field.
As you will see in this series, Ktor is very flexible, thus, it lets us choose what server engine we want to run our project on. So far I’ve had success using Netty, it runs great on platforms like Heroku, which you can easily deploy through their CLI. Don’t worry about this now though, we’ll cover it in a future post.
Next, we need to define which features we’d like to have installed on our project. These can all be installed later as well, but for today’s article, we’ll check everything we need to get started with a proper API.
From the server panel of features you should select:
- Default Headers (From “Features”)
- Routing (From “Features”)
- Content Negotiation (From “Content Negotiation”)
That’s it, that’s all we need to install to start responding to HTTP requests. Now click “Build” on the bottom left of the screen and extract the contents of the folder wherever you like.
Open up your project in IntelliJ, and wait for Gradle to resolve itself. This might take a while, especially if it’s your first time downloading all the artifacts.
Let’s start with a tour of our project. First, take a look at the “resources” folder. We have a file called “application.conf”, the name says everything, but think about this as your environment config file, you should use this file to store third-party API keys, server settings, and basically anything you deem necessary. You also use this file to say what modules Ktor should load on startup.
Note that I’ve added a node called “myCustomNode”, this means that we can declare anything we need inside this file and we’ll be able to retrieve it at runtime.
Now, pay attention to the node module loaded on line 11, then head to “src/Application.kt”, notice that the module loaded references exactly this file, plus “.module” at the end.
Why “.module”? You might ask. Well, Ktor makes heavy use of a language feature called “Extension functions”, you can read more about it here. Notice that, inside “Application.kt”, we created the “.module” function for the “Application” class, thus, that’s why we need to reference it inside the “application.conf” file.
Before we proceed, let’s test if everything is working fine. Head to the Gradle menu, expand the “Tasks” menu, then “Application”, and double-click “run”.
The server should start and your terminal will look like this.
If you’re having trouble running the application, don’t worry, it’s probably just some misconfiguration in your JDK, drop a comment and let me know how I can help.
Anyways, head to http://localhost:8080/, and there you have it. The good old “HELLO WORLD!”
Stop the server, and let’s talk about the composition of “Application.kt”. First, you might notice a couple of annotations above the module we’re declaring. The first one is purely so IntelliJ won’t bother us about not explicitly using the function we’re declaring. At the time of writing, the IDE still doesn’t have knowledge about the link between “application.conf” and “Application.kt”, so it’ll warn us about it. You may remove it if compilation warnings don’t scare you.
The second annotation is saying that, at compile-time, the compiler should generate overloads of that method, because the JVM doesn’t support optional parameters by default. This is because our “testing” parameter is optional. We won’t cover testing at this point, so let’s get rid of it and clean up our file a little bit. The original “Application.kt” looked like this.
After the cleanup, it should be looking like this.
I’ve removed the annotation and the optional parameter, notice I’ve also removed the package name of the file, you should reflect this change on your “application.conf” file as well. Of course, this is a matter of preference, so do it if you like it this way.
If you’ve removed the package name of your “Application.kt” file, you should change the loaded module from “com.example.ApplicationKt.module” to simply “ApplicationKt.module”
Features
You might remember we installed a couple of features while generating the project, this is where we see those features come to life. Every feature you add to your project with the dependency manager of your choice needs to be installed on startup. That’s what we’re doing when we write “install()” inside our code.
This method receives an object of type ApplicationFeature<T>, and the SAM interface (What is a SAM interface?) after it gives us an opportunity to configure the feature as needed.
Notice that, after we installed the “DefaultHeaders” feature, we could set other headers as well, because the interface provides us with a Configuration object.
This is the philosophy of operation with every feature you install on Ktor. Let’s have a look at the ContentNegotiation feature and get to writing some code.
ContentNegotiation is the feature that allows us to serialize content to our clients, but, as I mentioned before, Ktor is very flexible, and it gives us the opportunity to use whatever serialization engine we like.
We’ll be using an official library called “kotlinx.serialization” to serialize our data as JSON. For that, head to your “build.gradle” file. And apply the changes mentioned below.
Then, load the Gradle changes.
Notice, “json()” is now importable. Go ahead and import it.
That’s it, we’re ready to send JSON responses to our clients.
Don’t forget to annotate the classes you’d like to serialize as JSON with the @Serializable annotation.
Routing
You may have noticed that our hello world endpoint is registered right here in our “Application.kt” file, that’s no good when your API has many endpoints, so I’ll show you how to group routes in separate files. Go ahead and create a package called “dog” and a Kotlin file called “DogRoute”, and another Kotlin file called “Dog” (this should be a data class), this is what your structure should look like.
Your “Dog.kt” file should look like this.
And your “DogRoutes.kt” file should look like this.
Then, go to your “Application.kt” and declare the “dogRoutes()” instead of explicitly creating the routes as it was before.
Now, let’s create our “DogService.kt”, this class will be responsible for giving us the data we’ll be rendering with our router. Think about “DogRoutes.kt” as your Controller in the MVC architecture. Of course, you’ll be saving “DogService.kt” inside the “dog” package.
Let’s go ahead and apply these methods in “DogRoutes.kt”.
Now let’s run our application and test everything out. I’ll be using Insomnia for this, but you can use anything that floats your boat.
As you can see, everything is working great! Before we wrap this up, there are a couple of loose knots on our application that I need to point out:
- We need to create “DogService” manually every time we need it. That’s the topic of our next article: Dependency Injection using Koin;
- Let’s be honest, this is useless without a persistence layer, that is why we’re also covering this in a future post, using Ktorm.
I hope you have been able to take the first steps with Ktor today, if you need anything, feel free to contact me for further information.
I should also point out a couple of great study materials if you want to learn more about Kotlin or Ktor.
- https://ktor.io/
- https://kotlinlang.org/docs/home.html
- https://github.com/Kotlin/kotlinx.serialization
You can find the entire source code here.
Until the next time!