Kotlin - Ktor
Overview
Ktor is a modern, asynchronous web framework for Kotlin that makes it easy to build web applications and APIs. This tutorial covers Ktor setup, routing, serialization, authentication, testing, and building production-ready applications with coroutines.
🎯 Learning Objectives:
- Understand Ktor architecture and setup
- Learn routing and HTTP handling
- Master serialization with kotlinx.serialization
- Implement authentication and authorization
- Build and test Ktor applications effectively
Getting Started with Ktor
Project Setup
// build.gradle.kts
plugins {
kotlin("jvm") version "1.9.10"
id("io.ktor.plugin") version "2.3.7"
kotlin("plugin.serialization") version "1.9.10"
application
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
// Ktor Server
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-server-config-yaml")
// Content Negotiation & Serialization
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
// Authentication
implementation("io.ktor:ktor-server-auth-jvm")
implementation("io.ktor:ktor-server-auth-jwt-jvm")
// CORS
implementation("io.ktor:ktor-server-cors-jvm")
// Status Pages
implementation("io.ktor:ktor-server-status-pages-jvm")
// Call Logging
implementation("io.ktor:ktor-server-call-logging-jvm")
// Validation
implementation("io.ktor:ktor-server-request-validation-jvm")
// Database
implementation("org.jetbrains.exposed:exposed-core:0.44.1")
implementation("org.jetbrains.exposed:exposed-dao:0.44.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.44.1")
implementation("org.jetbrains.exposed:exposed-java-time:0.44.1")
implementation("com.h2database:h2:2.2.224")
implementation("com.zaxxer:HikariCP:5.0.1")
// Logging
implementation("ch.qos.logback:logback-classic:1.4.14")
// Testing
testImplementation("io.ktor:ktor-server-tests-jvm")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.9.10")
testImplementation("io.ktor:ktor-client-content-negotiation-jvm")
}
application {
mainClass.set("com.example.ApplicationKt")
}
ktor {
fatJar {
archiveFileName.set("ktor-app.jar")
}
}
Basic Application Setup
// Application.kt
package com.example
import com.example.plugins.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
fun Application.module() {
configureHTTP()
configureSerialization()
configureMonitoring()
configureSecurity()
configureRouting()
configureDatabase()
}
Configuration
# application.yml
ktor:
application:
modules:
- com.example.ApplicationKt.module
development: true
server:
port: 8080
host: 0.0.0.0
database:
url: "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"
driver: "org.h2.Driver"
user: "sa"
password: ""
jwt:
secret: "your-256-bit-secret-key-here"
issuer: "ktor-app"
audience: "ktor-users"
realm: "ktor-api"
Routing and HTTP Handling
Basic Routing
// plugins/Routing.kt
package com.example.plugins
import com.example.routes.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureRouting() {
routing {
// Health check
get("/health") {
call.respond(mapOf("status" to "OK", "timestamp" to System.currentTimeMillis()))
}
// API routes
route("/api/v1") {
userRoutes()
postRoutes()
authRoutes()
}
// Static content
staticResources("/static", "static")
}
}
User Routes
// routes/UserRoutes.kt
package com.example.routes
import com.example.models.*
import com.example.services.UserService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject
fun Route.userRoutes() {
val userService by inject<UserService>()
route("/users") {
// Public routes
post {
val createUserRequest = call.receive<CreateUserRequest>()
try {
val user = userService.createUser(createUserRequest)
call.respond(HttpStatusCode.Created, ApiResponse.success(user))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error(e.message ?: "Invalid request"))
}
}
get("/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error("Invalid user ID"))
return@get
}
val user = userService.getUserById(id)
if (user != null) {
call.respond(ApiResponse.success(user))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error("User not found"))
}
}
// Protected routes
authenticate("auth-jwt") {
get {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
val size = call.request.queryParameters["size"]?.toIntOrNull() ?: 10
val search = call.request.queryParameters["search"]
val users = userService.getUsers(page, size, search)
call.respond(ApiResponse.success(users))
}
put("/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error("Invalid user ID"))
return@put
}
val updateRequest = call.receive<UpdateUserRequest>()
try {
val user = userService.updateUser(id, updateRequest)
if (user != null) {
call.respond(ApiResponse.success(user))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error("User not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error(e.message ?: "Update failed"))
}
}
delete("/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error("Invalid user ID"))
return@delete
}
val deleted = userService.deleteUser(id)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error("User not found"))
}
}
}
}
}
Data Models and Serialization
Data Classes with Serialization
// models/User.kt
package com.example.models
import kotlinx.serialization.Serializable
import java.time.LocalDateTime
@Serializable
data class User(
val id: Long = 0,
val name: String,
val email: String,
@Serializable(with = LocalDateTimeSerializer::class)
val createdAt: LocalDateTime = LocalDateTime.now(),
@Serializable(with = LocalDateTimeSerializer::class)
val updatedAt: LocalDateTime = LocalDateTime.now()
)
@Serializable
data class CreateUserRequest(
val name: String,
val email: String
) {
fun validate(): List<String> {
val errors = mutableListOf<String>()
if (name.isBlank()) errors.add("Name is required")
if (name.length > 100) errors.add("Name must not exceed 100 characters")
if (email.isBlank()) errors.add("Email is required")
if (!email.contains("@")) errors.add("Email must be valid")
return errors
}
}
@Serializable
data class UpdateUserRequest(
val name: String,
val email: String
)
@Serializable
data class UserResponse(
val id: Long,
val name: String,
val email: String,
@Serializable(with = LocalDateTimeSerializer::class)
val createdAt: LocalDateTime,
val postCount: Int = 0
)
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val message: String? = null,
val errors: List<String> = emptyList()
) {
companion object {
fun <T> success(data: T, message: String? = null) = ApiResponse(
success = true,
data = data,
message = message
)
fun error(message: String, errors: List<String> = emptyList()) = ApiResponse<Nothing>(
success = false,
message = message,
errors = errors
)
}
}
@Serializable
data class PaginatedResponse<T>(
val items: List<T>,
val page: Int,
val size: Int,
val total: Long,
val totalPages: Int
)
// Custom serializer for LocalDateTime
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
override val descriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalDateTime {
return LocalDateTime.parse(decoder.decodeString())
}
}
Serialization Configuration
// plugins/Serialization.kt
package com.example.plugins
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import kotlinx.serialization.json.Json
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = true
})
}
}
Database Integration with Exposed
Database Configuration
// plugins/Database.kt
package com.example.plugins
import com.example.database.DatabaseFactory
import io.ktor.server.application.*
fun Application.configureDatabase() {
DatabaseFactory.init(environment.config)
}
// database/DatabaseFactory.kt
package com.example.database
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.ktor.server.config.*
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object DatabaseFactory {
fun init(config: ApplicationConfig) {
val driverClassName = config.property("database.driver").getString()
val jdbcURL = config.property("database.url").getString()
val user = config.property("database.user").getString()
val password = config.property("database.password").getString()
val database = Database.connect(createHikariDataSource(jdbcURL, driverClassName, user, password))
transaction(database) {
SchemaUtils.create(Users, Posts)
}
}
private fun createHikariDataSource(
url: String,
driver: String,
user: String,
password: String
) = HikariDataSource(HikariConfig().apply {
driverClassName = driver
jdbcUrl = url
username = user
this.password = password
maximumPoolSize = 3
isAutoCommit = false
transactionIsolation = "TRANSACTION_REPEATABLE_READ"
validate()
})
}
// database/Tables.kt
package com.example.database
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.javatime.datetime
import java.time.LocalDateTime
object Users : LongIdTable() {
val name = varchar("name", 100)
val email = varchar("email", 255).uniqueIndex()
val createdAt = datetime("created_at").default(LocalDateTime.now())
val updatedAt = datetime("updated_at").default(LocalDateTime.now())
}
object Posts : LongIdTable() {
val title = varchar("title", 200)
val content = text("content")
val userId = reference("user_id", Users)
val createdAt = datetime("created_at").default(LocalDateTime.now())
}
Data Access Layer
// services/UserService.kt
package com.example.services
import com.example.database.Users
import com.example.models.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.LocalDateTime
class UserService {
suspend fun createUser(request: CreateUserRequest): User {
// Validate request
val errors = request.validate()
if (errors.isNotEmpty()) {
throw IllegalArgumentException(errors.joinToString(", "))
}
return transaction {
// Check if email already exists
val existingUser = Users.select { Users.email eq request.email }.singleOrNull()
if (existingUser != null) {
throw IllegalArgumentException("User with email ${request.email} already exists")
}
val id = Users.insertAndGetId {
it[name] = request.name
it[email] = request.email
it[createdAt] = LocalDateTime.now()
it[updatedAt] = LocalDateTime.now()
}
User(
id = id.value,
name = request.name,
email = request.email,
createdAt = LocalDateTime.now(),
updatedAt = LocalDateTime.now()
)
}
}
suspend fun getUserById(id: Long): User? {
return transaction {
Users.select { Users.id eq id }
.singleOrNull()
?.let { row ->
User(
id = row[Users.id].value,
name = row[Users.name],
email = row[Users.email],
createdAt = row[Users.createdAt],
updatedAt = row[Users.updatedAt]
)
}
}
}
suspend fun getUsers(page: Int, size: Int, search: String?): PaginatedResponse<UserResponse> {
return transaction {
val query = if (search.isNullOrBlank()) {
Users.selectAll()
} else {
Users.select { Users.name like "%$search%" or (Users.email like "%$search%") }
}
val total = query.count()
val users = query
.limit(size, offset = (page * size).toLong())
.orderBy(Users.createdAt, SortOrder.DESC)
.map { row ->
UserResponse(
id = row[Users.id].value,
name = row[Users.name],
email = row[Users.email],
createdAt = row[Users.createdAt]
)
}
PaginatedResponse(
items = users,
page = page,
size = size,
total = total,
totalPages = ((total + size - 1) / size).toInt()
)
}
}
suspend fun updateUser(id: Long, request: UpdateUserRequest): User? {
return transaction {
val existingUser = Users.select { Users.id eq id }.singleOrNull()
?: return@transaction null
// Check if email is being changed and already exists
if (request.email != existingUser[Users.email]) {
val emailExists = Users.select { (Users.email eq request.email) and (Users.id neq id) }
.singleOrNull()
if (emailExists != null) {
throw IllegalArgumentException("User with email ${request.email} already exists")
}
}
Users.update({ Users.id eq id }) {
it[name] = request.name
it[email] = request.email
it[updatedAt] = LocalDateTime.now()
}
User(
id = id,
name = request.name,
email = request.email,
createdAt = existingUser[Users.createdAt],
updatedAt = LocalDateTime.now()
)
}
}
suspend fun deleteUser(id: Long): Boolean {
return transaction {
Users.deleteWhere { Users.id eq id } > 0
}
}
}
Authentication and Security
JWT Authentication
// plugins/Security.kt
package com.example.plugins
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
fun Application.configureSecurity() {
val jwtSecret = environment.config.property("jwt.secret").getString()
val jwtIssuer = environment.config.property("jwt.issuer").getString()
val jwtAudience = environment.config.property("jwt.audience").getString()
val jwtRealm = environment.config.property("jwt.realm").getString()
authentication {
jwt("auth-jwt") {
realm = jwtRealm
verifier(
JWT
.require(Algorithm.HMAC256(jwtSecret))
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.build()
)
validate { credential ->
if (credential.payload.getClaim("username").asString() != "") {
JWTPrincipal(credential.payload)
} else {
null
}
}
}
}
}
// Auth service
package com.example.services
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.example.models.LoginRequest
import com.example.models.LoginResponse
import java.util.*
class AuthService(
private val userService: UserService,
private val jwtSecret: String,
private val jwtIssuer: String,
private val jwtAudience: String
) {
suspend fun login(request: LoginRequest): LoginResponse? {
// In a real app, you'd verify password hash
val user = userService.getUserByEmail(request.email) ?: return null
val token = JWT.create()
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.withClaim("username", user.email)
.withClaim("userId", user.id)
.withExpiresAt(Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) // 24 hours
.sign(Algorithm.HMAC256(jwtSecret))
return LoginResponse(
token = token,
user = user,
expiresIn = 24 * 60 * 60 // 24 hours in seconds
)
}
}
Authentication Routes
// routes/AuthRoutes.kt
package com.example.routes
import com.example.models.LoginRequest
import com.example.models.ApiResponse
import com.example.services.AuthService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject
fun Route.authRoutes() {
val authService by inject<AuthService>()
route("/auth") {
post("/login") {
try {
val loginRequest = call.receive<LoginRequest>()
val response = authService.login(loginRequest)
if (response != null) {
call.respond(ApiResponse.success(response))
} else {
call.respond(
HttpStatusCode.Unauthorized,
ApiResponse.error("Invalid credentials")
)
}
} catch (e: Exception) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error("Invalid request format")
)
}
}
post("/logout") {
// In a real app, you might maintain a blacklist of tokens
call.respond(ApiResponse.success("Logged out successfully"))
}
}
}
Error Handling and Status Pages
Global Error Handling
// plugins/HTTP.kt
package com.example.plugins
import com.example.models.ApiResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
fun Application.configureHTTP() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.ContentType)
allowCredentials = true
allowNonSimpleContentTypes = true
anyHost() // In production, specify allowed hosts
}
install(StatusPages) {
exception<IllegalArgumentException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error(cause.message ?: "Bad request")
)
}
exception<IllegalAccessException> { call, cause ->
call.respond(
HttpStatusCode.Forbidden,
ApiResponse.error(cause.message ?: "Access denied")
)
}
exception<Exception> { call, cause ->
call.respond(
HttpStatusCode.InternalServerError,
ApiResponse.error("Internal server error")
)
call.application.log.error("Unhandled exception", cause)
}
status(HttpStatusCode.NotFound) { call, _ ->
call.respond(
HttpStatusCode.NotFound,
ApiResponse.error("Endpoint not found")
)
}
status(HttpStatusCode.Unauthorized) { call, _ ->
call.respond(
HttpStatusCode.Unauthorized,
ApiResponse.error("Authentication required")
)
}
}
}
Monitoring and Logging
Request Logging and Monitoring
// plugins/Monitoring.kt
package com.example.plugins
import io.ktor.server.application.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.request.*
import org.slf4j.event.Level
fun Application.configureMonitoring() {
install(CallLogging) {
level = Level.INFO
filter { call -> call.request.path().startsWith("/api") }
format { call ->
val status = call.response.status()
val httpMethod = call.request.httpMethod.value
val path = call.request.path()
val userAgent = call.request.headers["User-Agent"]
"$httpMethod $path - $status - $userAgent"
}
}
}
Testing
API Testing
// test/ApplicationTest.kt
package com.example
import com.example.models.CreateUserRequest
import com.example.models.ApiResponse
import com.example.plugins.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.testing.*
import kotlinx.serialization.json.Json
import kotlin.test.*
class ApplicationTest {
@Test
fun testHealthEndpoint() = testApplication {
application {
configureRouting()
}
client.get("/health").apply {
assertEquals(HttpStatusCode.OK, status)
val response = body<Map<String, Any>>()
assertEquals("OK", response["status"])
}
}
@Test
fun testCreateUser() = testApplication {
application {
module()
}
val client = createClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
val createRequest = CreateUserRequest("John Doe", "[email protected]")
client.post("/api/v1/users") {
contentType(ContentType.Application.Json)
setBody(createRequest)
}.apply {
assertEquals(HttpStatusCode.Created, status)
val response = body<ApiResponse<Any>>()
assertTrue(response.success)
}
}
@Test
fun testUserNotFound() = testApplication {
application {
module()
}
client.get("/api/v1/users/999").apply {
assertEquals(HttpStatusCode.NotFound, status)
}
}
@Test
fun testInvalidUserCreation() = testApplication {
application {
module()
}
val client = createClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
val invalidRequest = CreateUserRequest("", "invalid-email")
client.post("/api/v1/users") {
contentType(ContentType.Application.Json)
setBody(invalidRequest)
}.apply {
assertEquals(HttpStatusCode.BadRequest, status)
}
}
}
Client-Side Usage
Ktor Client
// client/ApiClient.kt
package com.example.client
import com.example.models.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
class ApiClient(
private val baseUrl: String = "http://localhost:8080",
private val token: String? = null
) {
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
})
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.INFO
}
token?.let { authToken ->
install(Auth) {
bearer {
loadTokens {
BearerTokens(authToken, authToken)
}
}
}
}
}
suspend fun createUser(request: CreateUserRequest): ApiResponse<User> {
return client.post("$baseUrl/api/v1/users") {
contentType(ContentType.Application.Json)
setBody(request)
}.body()
}
suspend fun getUser(id: Long): ApiResponse<User> {
return client.get("$baseUrl/api/v1/users/$id").body()
}
suspend fun getUsers(page: Int = 0, size: Int = 10, search: String? = null): ApiResponse<PaginatedResponse<UserResponse>> {
return client.get("$baseUrl/api/v1/users") {
parameter("page", page)
parameter("size", size)
search?.let { parameter("search", it) }
}.body()
}
suspend fun login(request: LoginRequest): ApiResponse<LoginResponse> {
return client.post("$baseUrl/api/v1/auth/login") {
contentType(ContentType.Application.Json)
setBody(request)
}.body()
}
fun close() {
client.close()
}
}
// Usage example
suspend fun main() {
val client = ApiClient()
try {
// Create a user
val createRequest = CreateUserRequest("Jane Doe", "[email protected]")
val createResponse = client.createUser(createRequest)
println("Created user: ${createResponse.data}")
// Get users
val usersResponse = client.getUsers(page = 0, size = 5)
println("Users: ${usersResponse.data?.items}")
} finally {
client.close()
}
}
Production Deployment
Docker Configuration
# Dockerfile
FROM openjdk:17-jre-slim
WORKDIR /app
COPY build/libs/ktor-app.jar app.jar
COPY src/main/resources/application.yml application.yml
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
# docker-compose.yml
version: '3.8'
services:
ktor-app:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=jdbc:postgresql://postgres:5432/ktordb
- DATABASE_USER=postgres
- DATABASE_PASSWORD=password
- JWT_SECRET=your-production-secret-key
depends_on:
- postgres
networks:
- ktor-network
postgres:
image: postgres:15
environment:
- POSTGRES_DB=ktordb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- ktor-network
volumes:
postgres_data:
networks:
ktor-network:
Best Practices
Performance and Optimization
// Use connection pooling
val database = Database.connect(createHikariDataSource(...))
// Implement caching
class CachedUserService(private val userService: UserService) {
private val cache = mutableMapOf<Long, User>()
suspend fun getUserById(id: Long): User? {
return cache[id] ?: userService.getUserById(id)?.also { cache[id] = it }
}
}
// Use coroutines for concurrent operations
suspend fun getUsersWithPosts(userIds: List<Long>): List<UserWithPosts> = coroutineScope {
userIds.map { userId ->
async {
val user = userService.getUserById(userId)
val posts = postService.getPostsByUserId(userId)
UserWithPosts(user, posts)
}
}.awaitAll()
}
// Implement request validation
fun Application.configureValidation() {
install(RequestValidation) {
validate<CreateUserRequest> { request ->
val errors = request.validate()
if (errors.isNotEmpty()) {
ValidationResult.Invalid(errors)
} else {
ValidationResult.Valid
}
}
}
}
Key Takeaways
- Ktor provides a lightweight, coroutine-based framework for Kotlin web development
- Use kotlinx.serialization for efficient JSON handling
- Implement proper error handling with StatusPages plugin
- Leverage coroutines for asynchronous operations
- Use Exposed for type-safe database operations
- Implement comprehensive testing with Ktor's testing utilities
- Follow REST API conventions and proper HTTP status codes
Practice Exercises
- Build a complete blog API with users, posts, comments, and tags
- Implement file upload and download functionality
- Add WebSocket support for real-time features
- Create a microservice architecture with multiple Ktor applications
Quiz
- What are the main advantages of Ktor over traditional servlet-based frameworks?
- How does Ktor handle asynchronous operations?
- What's the recommended way to handle database operations in Ktor?
Show Answers
- Ktor is lightweight, uses Kotlin coroutines for async operations, has a modular plugin system, and provides excellent Kotlin integration with type safety.
- Ktor uses Kotlin coroutines throughout, allowing non-blocking operations with suspend functions and structured concurrency.
- Use Exposed ORM with transaction blocks or implement a repository pattern with proper connection pooling and async database operations.