SERIES: Building a backend with Ktor
- Part 1: Structuring a Ktor project
- Part 2: How to persist Ktor logs
It’s been a few months since I’ve started working with Ktor to build the backend of Revelop. We decided to go with Ktor because it is a lightweight framework, easy to use and with a gentle learning curve even for a mobile developer.
Today I decided to start a series of posts dedicated to Ktor. With these articles, I want to cover all the topics that made me struggle during development and that was not easy to achieve out of the box. To cite a few: using an in-memory database for testing, handling database migration, setting up logging on disk and using dependency injection.
In this first instance of the series, I will show how I’ve structured the Ktor project I’m working on. I’ll cover dependency injection, configurations, and testing.
But before moving on, a quick introduction about Ktor is mandatory.
Ktor is an asynchronous framework for creating microservices, web applications, and more. It’s fun, free, and open source.
Ktor is a lightweight framework that lets easily build backends, web applications, mobile and browser applications. It can be used to create both server and client-side applications (it is compatible with Kotlin Multiplatform as well). Ktor is highly configurable with extensions and it is possible to configure a custom pipeline through a Kotlin DSL. And finally, Ktor is truly asynchronous and uses Kotlin Coroutines to make the development easier without the callback hell.
This is “an elevator pitch” of Ktor, to know all the details I’ll suggest looking to the documentation.
Create a new Ktor Project
The starting point of a Ktor project definitely lies in the wizard included in IntelliJ. The wizard lets you choose between all the different features that Ktor provides and it will generate a bare-bone project ready to be used.
If you don’t like IntelliJ, the wizard is also available on start.ktor.io.
The project that I built as a reference for this series contains a few set of features:
- Call Logging
- Content Negotiation
The project is a simple backend that returns random Chuck Norris jokes. The jokes are saved in a database and they came from the Chuck Norris IO project.
The wizard creates a default
Application.kt file that contains the
module function that initializes the server pipeline, install the selected features, register the routes, etc. In this function, all the configurations and the classes needed to run the server must be provided or initialized.
Dependency Injection with Koin
Before moving on, it is a good idea to setup Dependency Injection. I’ll use Koin, that has built-in support for Ktor.
To use Koin, it is necessary to install the appropriate feature inside the
module function. I recommend doing it as the first thing in the setup pipeline.
The Koin module is defined in a separate file, just to keep the
Application class and the Ktor
module function as clean as possible.
After that, the dependency graph is built, and inside
Route scope, it is possible to retrieve the dependencies like in a
For more information about Koin on Ktor, refer to the documentation
On Ktor it is possible to set some configurations, like host address and port, in code (if using the
embeddedServer) or in an external file (
application.conf) with the HOCON format (if using the
The wizard automatically creates an
application.conf file in the application
This configuration file will be automatically loaded and parsed by Ktor when the server is started. It is also possible to provide a custom configuration file instead of the one from resources with a command-line argument:
This is helpful for example to provide different configurations for databases or for external service (in part 3 I’ll show a use case of this feature).
But, besides the default value provided by the framework, it is possible to create custom configurations to use later in the code. For example, I’ve created a new section with a Boolean field that will indicate if the instance is running on a staging or production server.
Every section will be mapped in the code with a
that is contained in a wider class, named
AppConfig with all the different custom configurations.
The fields of this class then will be initialized inside the
With this setup, when a configuration field must be accessed, the AppConfig class can be simply retrieved from Koin.
The structure of a project is a completely subjective topic and it does not invalidate the functionalities nor the correct behavior. However, I think that properly structuring a project is beneficial for better maintainability and scalability.
Here, I will share a structure that worked for me and that I found interesting. If you have any suggestion about alternative structures or if you notice something wrong in what I’m sharing, feel free to reach me out on Twitter @marcoGomier.
Application.kt file is the entrypoint for the server, I left it at the root level of the
src > main > kotlin > <package-name> folder.
Then, I’ve created a bunch of folders that contain “configuration code”:
di: it contains the code where the
Koinmodules are defined, as described above
config: it contains the code that maps the configuration fields from the
application.conffile, as described above
database: it contains the code necessary to open (and close) the connection to the database. I’ll describe it in the next articles of the series.
Then, I’ve created the
features folder. This top-level folder contains, as the name suggests, all the different features of the backend. For example, if the backend provides a set of API calls to authenticate a user, to handle jokes (get, create, delete, etc), and to handle the user (logout, update some settings, etc) there will be three different folders:
In the sample project, the backend will provide only an API to get a random Joke, so there will be only a folder named
jokes under the
The structure of every “feature folder” will follow some principles of the Clean Architecture. I’ve decided to stick with it because I’ve used it in some Android projects and I like it. But, this will be an adapted solution with only some aspects of the architecture. If you don’t know Clean Architecture and you want to know more about it, I suggest giving a look at the Uncle Bob’s article and, if you are an Android developer, to this Ray Wenderich’s article.
The layers of Clean Architecture that I’ve used here are 3:
The data layer contains the definitions of all the data sources. In this case, all the needed data are contained in the database, so there will be only code needed to interact with the database.
If the backend needs also to retrieve data from other APIs, here there will be also a remote data source.
The domain layer contains the models, the business logic, and the mappers between the DAOs and the DTOs. The business logic is implemented by following the Repository pattern. The repository will contain the code necessary to retrieve, save and manipulate data from the data sources.
And at the end, the presentation layer. Since this is not an application with a user interface, I decided to change the name from presentation to resource. In this layer, there will be the definitions of the REST endpoints that the backend exposes. I’ve decided to use the word resource because I like to think that REST endpoints are resources that give or handle data. This is just a personal opinion, for example, you can call these layer `controller or whatever.
JokeResource file will contain the classes that define each route and an extension function of
Route that contains the definition of every endpoint.
Using an extension function, unlock the possibility to have a much cleaner module function: in this way, it is possible to define the endpoints in different files and call them inside the
routing block in the
And as reference, here’s the entire structure that I’ve described:
And last but not least, testing. Ktor is designed to allow easily testable applications. It does not create a web server but it hooks directly into the internal mechanism with a
TestEngine. In this way, the execution of tests will be quicker rather than spinning up a complete web server for testing.
withTestApplication function it is possible to set up a test environment and then, with the
handleRequest function it is possible to perform the request and verify that the results are the same as expected.
However, before testing the endpoints, it is necessary to set up the dependencies that the system under test will use and all the custom configurations defined in the
To populate the configurations, the
MapApplicationConfig can be used and passed to the
The dependencies instead will be automatically provided by Koin. But, to provide a fake implementation of a dependency, some changes must be made.
As described above, the Koin module is ”hardcoded” inside the Ktor module function (to be precise is defined in another file but not injected in the constructor).
To modify the Koin module during testing, I’ve modified the Ktor
module function to accept a list of Koin modules. This list has as the default value the Koin module that was previously hardcoded.
In this way, during testing, it is possible to change one or more dependencies.
Although it seems a boring task, structuring a project is a very important thing. In fact, starting with an unsafe foundation will put your product in danger, making it unscalable and hard to maintain throughout the time. However, there isn’t a right way to structure a project because every project is different and has different needs. What I suggest to do, is to find the right solution for your needs and the structure that I’ve shared here is the one that satisfied me.
And that’s it for today. You can find the code mentioned in the article on GitHub.
In the next episodes, I’ll cover logging and databases. You can follow me on Twitter to know when I’ll publish the next episode.