We already know that Ktor can create server applications (running on Netty, Tomcat, Jetty, or Coroutine I/O) listening to some port and endpoints. It is important to note that a Ktor application comprises modules, which themselves can comprise plugins and functions (for example routing, sessions, logging). As we know the routing function serves to configure the endpoints of our application.
Ktor Modules
Modules in Ktor are the foundational concept for building applications. A module is simply an extension function of the Application class. They server as the configuration entry point for the Application.
Since modules are extensions of the Application class, they act as the receiver for the same Application instance created during server startup, allowing us to apply configurations directly to it.
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:
When using embeddedServer, you can either define your modules using a lambda for the module parameter or pass a module function reference (allowing us to group multiple sub-modules into a "main" module), as shown below:
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
fun Application.module() {
configureRouting()
configureSomeSpecialRoutes()
anotherOneModule()
andAnotherOneModule()
}If you are using an external configuration, you can use application.yaml (or application.conf). You can specify the fully qualified module name(s) or a designated "main" module function as shown:
application.yaml:
ktor:
application:
modules:
- com.example.ApplicationKt.module
deployment:
port: 8080Application.kt:
package com.example
// ...
fun main(args: Array<String>) {
io.ktor.server.netty.EngineMain.main(args)
}
fun Application.module() {
configureRouting()
configureSomeSpecialRoutes()
anotherOneModule()
andAnotherOneModule()
}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 and declare them at the application level. Our routing configuration module 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:
// MusicRoutes.kt
fun Application.musicRoutes() {
routing {
createMusicRoute()
musicByIdRoute()
}
}Then we will have to install this in the main module:
fun Application.module() {
musicRoutes()
// other plugins
}It is important to clarify that Ktor will detect if we install a plugin twice and throw a DuplicateApplicationPluginException.
routing first checks whether the Routing plugin is already installed. If it is, it simply configures the existing installation. If not, it installs and configures it. This is why you can call routing multiple times without triggering a DuplicateApplicationPluginException.
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.