Kotlin Ktor Framework
Introduction
Ktor is a lightweight, modern framework for building asynchronous server-side applications in Kotlin. Developed by JetBrains, the creators of Kotlin, Ktor leverages Kotlin's language features like coroutines, creating a highly efficient and intuitive API for developing web applications. Unlike traditional Java-based frameworks, Ktor was designed from the ground up to be completely Kotlin-native, offering a more streamlined development experience.
Whether you're building a REST API, websocket service, or a full-stack web application, Ktor provides the tools and flexibility needed to create robust backend solutions with minimal boilerplate code. This guide will walk you through the basics of Ktor and help you get started with building your first Ktor application.
Getting Started with Ktor
Setting Up a Ktor Project
The easiest way to start a new Ktor project is by using the Ktor Project Generator provided by JetBrains. Alternatively, you can set up a Gradle project manually.
Here's how you can set up a basic Ktor project using Gradle:
// build.gradle.kts
plugins {
kotlin("jvm") version "1.8.20"
id("io.ktor.plugin") version "2.3.0"
}
group = "com.example"
version = "0.0.1"
application {
mainClass.set("com.example.ApplicationKt")
}
repositories {
mavenCentral()
}
dependencies {
implementation("io.ktor:ktor-server-core:2.3.0")
implementation("io.ktor:ktor-server-netty:2.3.0")
implementation("ch.qos.logback:logback-classic:1.4.7")
testImplementation("io.ktor:ktor-server-test-host:2.3.0")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.8.20")
}
Creating Your First Ktor Application
Now let's create a basic Ktor application that responds to HTTP requests:
// src/main/kotlin/com/example/Application.kt
package com.example
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun main() {
embeddedServer(Netty, port = 8080) {
routing {
get("/") {
call.respondText("Hello, World!")
}
}
}.start(wait = true)
}
When you run this application and navigate to http://localhost:8080
in your browser, you'll see the text "Hello, World!" displayed.
Core Concepts in Ktor
Application Structure
A Ktor application is built around several key concepts:
- Engine - Handles connections and dispatches requests (e.g., Netty, Tomcat)
- Application - The core container that holds all routes, plugins, and configurations
- Routing - Defines how different HTTP requests are handled
- Modules - Logical units containing configuration and features
- Plugins - Add functionality to your application (authentication, serialization, etc.)
Routing
Routing is one of the most important concepts in Ktor. It defines how your application responds to different HTTP requests:
routing {
// Handle GET requests to the root path
get("/") {
call.respondText("Hello, World!")
}
// Handle GET requests to /users path
get("/users") {
call.respondText("List of users")
}
// Handle POST requests to /users path
post("/users") {
val userData = call.receiveText()
call.respondText("Created user from: $userData")
}
// Path parameters
get("/users/{id}") {
val id = call.parameters["id"]
call.respondText("User details for ID: $id")
}
}
Using Plugins
Plugins are a powerful feature in Ktor that allow you to enhance your application with additional functionality:
fun main() {
embeddedServer(Netty, port = 8080) {
// Install ContentNegotiation plugin for JSON processing
install(ContentNegotiation) {
json()
}
// Install CallLogging plugin for request logging
install(CallLogging) {
level = Level.INFO
}
routing {
get("/") {
call.respondText("Hello, World!")
}
}
}.start(wait = true)
}
Make sure to add the necessary dependencies for the plugins:
// For ContentNegotiation with JSON
implementation("io.ktor:ktor-server-content-negotiation:2.3.0")
implementation("io.ktor:ktor-serialization-jackson:2.3.0")
// or
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0")
// For CallLogging
implementation("io.ktor:ktor-server-call-logging:2.3.0")
Building a RESTful API
Let's build a simple RESTful API for managing a collection of books:
package com.example
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class Book(val id: Int, val title: String, val author: String, val year: Int)
val books = mutableListOf(
Book(1, "Kotlin in Action", "Dmitry Jemerov", 2017),
Book(2, "Effective Kotlin", "Marcin Moskala", 2019),
Book(3, "Atomic Kotlin", "Bruce Eckel", 2021)
)
fun main() {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}
routing {
// Get all books
get("/books") {
call.respond(books)
}
// Get a specific book
get("/books/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID format")
return@get
}
val book = books.find { it.id == id }
if (book != null) {
call.respond(book)
} else {
call.respond(HttpStatusCode.NotFound, "Book not found")
}
}
// Create a new book
post("/books") {
val newBook = call.receive<Book>()
books.add(newBook)
call.respond(HttpStatusCode.Created, newBook)
}
// Update an existing book
put("/books/{id}") {
val id = call.parameters["id"]?.toIntOrNull() ?: return@put call.respond(
HttpStatusCode.BadRequest, "Invalid ID format")
val updatedBook = call.receive<Book>()
val index = books.indexOfFirst { it.id == id }
if (index == -1) {
call.respond(HttpStatusCode.NotFound, "Book not found")
} else {
books[index] = updatedBook
call.respond(updatedBook)
}
}
// Delete a book
delete("/books/{id}") {
val id = call.parameters["id"]?.toIntOrNull() ?: return@delete call.respond(
HttpStatusCode.BadRequest, "Invalid ID format")
val removed = books.removeIf { it.id == id }
if (removed) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, "Book not found")
}
}
}
}.start(wait = true)
}
To use this code, make sure to add the kotlinx serialization dependencies:
plugins {
kotlin("jvm") version "1.8.20"
id("io.ktor.plugin") version "2.3.0"
kotlin("plugin.serialization") version "1.8.20" // Add this line
}
dependencies {
// Other dependencies
implementation("io.ktor:ktor-server-content-negotiation:2.3.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0")
}
Authentication and Authorization
Securing your Ktor application is essential for most real-world applications. Ktor provides built-in support for different authentication methods:
fun Application.configureSecurity() {
install(Authentication) {
basic("basic-auth") {
realm = "Ktor Server"
validate { credentials ->
if (credentials.name == "admin" && credentials.password == "password") {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
jwt("jwt-auth") {
realm = "Ktor Server"
verifier(
JWT.require(Algorithm.HMAC256("secret"))
.withAudience("jwt-audience")
.withIssuer("jwt-issuer")
.build()
)
validate { credential ->
if (credential.payload.audience.contains("jwt-audience")) {
JWTPrincipal(credential.payload)
} else {
null
}
}
}
}
routing {
authenticate("basic-auth") {
get("/protected/basic") {
val principal = call.principal<UserIdPrincipal>()
call.respondText("Hello, ${principal?.name}!")
}
}
authenticate("jwt-auth") {
get("/protected/jwt") {
val principal = call.principal<JWTPrincipal>()
val username = principal?.payload?.getClaim("username")?.asString()
call.respondText("Hello, $username!")
}
}
}
}
Add these dependencies for authentication:
implementation("io.ktor:ktor-server-auth:2.3.0")
implementation("io.ktor:ktor-server-auth-jwt:2.3.0")
Testing in Ktor
Ktor provides a simple way to test your application using the testApplication
function:
class ApplicationTest {
@Test
fun testRoot() = testApplication {
application {
routing {
get("/") {
call.respondText("Hello, World!")
}
}
}
val response = client.get("/")
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("Hello, World!", response.bodyAsText())
}
@Test
fun testGetBooks() = testApplication {
application {
module() // Your application's main module
}
val response = client.get("/books")
assertEquals(HttpStatusCode.OK, response.status)
// Further assertions on the response body
}
}
Structuring a Larger Ktor Application
As your application grows, it's important to structure it properly. Here's a recommended approach:
// Application.kt
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
fun Application.module() {
configureSerialization()
configureMonitoring()
configureSecurity()
configureRouting()
}
// Plugins.kt
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}
fun Application.configureMonitoring() {
install(CallLogging)
}
fun Application.configureSecurity() {
// Authentication configuration
}
// Routing.kt
fun Application.configureRouting() {
routing {
bookRoutes()
userRoutes()
}
}
// BookRoutes.kt
fun Route.bookRoutes() {
route("/books") {
get {
// Get all books
}
get("/{id}") {
// Get a specific book
}
// Other book routes
}
}
// UserRoutes.kt
fun Route.userRoutes() {
route("/users") {
// User related routes
}
}
Deploying a Ktor Application
Ktor applications can be deployed in various ways:
-
Fat JAR: Package your application as a self-contained JAR file
kotlin// build.gradle.kts
application {
mainClass.set("com.example.ApplicationKt")
}
tasks {
val fatJar = register<Jar>("fatJar") {
dependsOn.addAll(listOf("compileJava", "compileKotlin", "processResources"))
archiveClassifier.set("standalone")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest { attributes(mapOf("Main-Class" to application.mainClass)) }
val sourcesMain = sourceSets.main.get()
val contents = configurations.runtimeClasspath.get()
.map { if (it.isDirectory) it else zipTree(it) } +
sourcesMain.output
from(contents)
}
build {
dependsOn(fatJar)
}
} -
Docker: Create a Docker container for your application
dockerfileFROM openjdk:11
EXPOSE 8080:8080
RUN mkdir /app
COPY ./build/libs/*-standalone.jar /app/application.jar
ENTRYPOINT ["java", "-jar", "/app/application.jar"]
Summary
In this guide, we've explored the Kotlin Ktor framework, a powerful tool for building backend applications with Kotlin. We've covered:
- Setting up a basic Ktor project
- Creating routes and handling HTTP requests
- Using plugins to extend functionality
- Building a complete RESTful API
- Adding authentication and security
- Testing Ktor applications
- Structuring larger applications
- Deployment options
Ktor's lightweight, flexible architecture and seamless integration with Kotlin's powerful features like coroutines make it an excellent choice for modern server-side development. Its non-invasive design allows developers to build applications exactly as they want, without forcing them into rigid patterns.
Additional Resources
Exercises
- Create a simple todo list API with endpoints to create, read, update, and delete tasks.
- Add JWT authentication to your API and restrict access to certain endpoints.
- Implement a WebSocket endpoint that broadcasts messages to all connected clients.
- Create a Ktor client that consumes an external API and processes the results.
- Extend the book API example to include categories and searching functionality.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)