We already know that Ktor is a server mechanism (with Tomcat, Jetty, Netty, or Coroutine I/O) listening to some port and endpoints. We should note that it comprises modules, which themselves comprise functions (for example routing, sessions, logging). As we know the routing function serves to configure the endpoints of our application.
Application Lifecycle
Ktor starts up and starts waiting for requests on a particular port. Ktor uses Pipelines to handle connections and other tasks. A Pipeline is a structure containing a sequence of functions that are called one after the other, but distributed over the phases of the application server.
For example, with an HTTP call, the application's Pipeline (also called ApplicationCallPipeline), will execute multiple phases, like treating the request, or even send the HTTP response.
We can also intercept existing phases. For example, the ApplicationCallPipeline, which receives the request. Let's write some code that will output the text about hooking the call to the console before the call is processed:
fun Application.configureRouting() {
routing {
get("/main") {
call.respond("OK")
}
}
}To do this, pass an intercept function to the embedded server that takes an object of type PipelinePhase. Inside, we will describe the actions that need to be performed. This function will be called when the corresponding phase is called, ApplicationCallPipeline.Call. Thus, we will intercept it. We often use this when developing third-party libraries for Ktor Framework.
embeddedServer(Tomcat) {
intercept(ApplicationCallPipeline.Call) {
println("intercepting request to path: ${call.request.path()}")
}
configureRouting()
} In console we will see, as expected:
Ktor defines a pipeline without a subject, and the ApplicationCall as a context defining five phases (Setup, Monitoring, Plugins, Call and Fallback) to be executed in specific order. Here you can find more info about ApplicationCall phases.
Pipelines are a tough topic to understand, so we will not go into it now. If you're interested, you can read more about it in the official documentation.
Ktor Modules
Modules in Ktor are the basic concept of building applications. When we met with routing before, we discussed the construction of modules — these are extension functions that are the receivers of the Application —. They are the configuration of the Application. Let's look at a simple routing example:
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
}
}
}That our function is an extension of the Application class allows us to run it inside the start function of our server, as we have done many times before.
The module in Ktor is simply an extension function of the Application class, nothing more.
We can create as many of these modules as we want and for completely different purposes. When we configure plugins, it's worthwhile to spread out their extension-functions across different files in case they get bigger. Similarly, it is worth doing with the most frequent customization-routes. Just as Spring has a separate class for the controller, in Ktor it makes sense to distribute different routes to different extension functions, and extension functions to different files.
fun Application.configureSomeSpecialRoutes() {
routing {
route("/special") {}
}
}That's how we'll apply them all in our application:
fun main() {
embeddedServer(Netty, port = 8080) {
configureRouting()
configureSomeSpecialRoutes()
anotherOneModule()
andAnotherOneModule()
}.start()
}anotherOneModule(), andAnotherOneModule() are the same type of modules as configureSomeSpecialRoutes().
Grouping routing definitions
To make the file structure of our project deterministic rather than having everything written in one file, we need to think about how to logically divide the code into files.
As the developers of Ktor recommend to us, there are several ways to divide up the code.
One option is to combine route-extension functions into one file, which are linked. If we have, for example, several routing groups such as News, Profile, Music.
Then there will be three corresponding files:
// NewsRoutes.kt
fun Route.newsByIdRoute() {
get("/news/{id}") {}
}
fun Route.createNewsRoute() {
post("/news") {}
}
// MusicRoutes.kt
fun Route.musicByIdRoute() {
get("/music/{id}") {}
}
fun Route.createMusicRoute() {
post("/music") {}
}
// ProfileRoutes.kt
fun Route.profileByIdRoute() {
get("/profile/{id}") {}
}
fun Route.createProfileRoute() {
post("/profile") {}
}The advantage of this option is that we can also group routing definitions and functionality for each file.
Let's follow the "group per file" scheme as described above. Even though the routes are in different files, we need to declare them at the application level. Thus, our application will look like this:
// Routing.kt
fun Application.configureRouting() {
routing {
musicByIdRoute()
createMusicRoute()
newsByIdRoute()
createNewsRoute()
profileByIdRoute()
createProfileRoute()
}
}Since we have routes that are grouped by file, we can take advantage of this and define the routing in each file. To do this, we can create an application extension and define routes:
fun Application.musicRoutes() {
routing {
createMusicRoute()
musicByIdRoute()
}
}Then we will have to install this as a plugin in the main function:
fun main() {
embeddedServer(Netty, port = 8080) {
musicRoutes()
// other plugins
}.start()
}It is important to clarify that Ktor will detect if we install the plugin twice and throw a DuplicateApplicationFeatureException.
Grouping by folders
Storing everything in one file or in several files at the same directory hierarchy level is inconvenient. Instead, you can use folders (that is, packages) to define different areas, and then put each route in a separate file:
package org.hyperskill.routes.music
import io.ktor.server.routing.*
// Create.kt
fun Route.createMusicRoute() {
post("/music") {}
}// MusicRoutes.kt
fun Application.musicRoutes() {
routing {
createMusicRoute()
musicByIdRoute()
}
}You can also group files based on features, architectural principles (such as MVC, for example). You can read more about it here.
Conclusion
Now we understand of how the Ktor app works better. We clarified the points about how to better structure our project files, so that when the project grows, there will be no problem with how to find the parts we need.