kotlinintermediate

Ktor Server — Routing and Middleware

Build HTTP servers with Ktor: routing DSL, middleware plugins, content negotiation, and error handling.

kotlin
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.server.request.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.http.*
import kotlinx.serialization.Serializable

@Serializable
data class TodoItem(val id: Int, val title: String, val done: Boolean = false)

@Serializable
data class CreateTodo(val title: String)

@Serializable
data class ApiError(val message: String, val code: Int)

// Custom exception
class NotFoundException(message: String) : RuntimeException(message)
class ValidationException(message: String) : RuntimeException(message)

fun Application.configurePlugins() {
    // JSON serialization
    install(ContentNegotiation) {
        json(kotlinx.serialization.json.Json {
            prettyPrint = true
            ignoreUnknownKeys = true
        })
    }

    // Error handling
    install(StatusPages) {
        exception<NotFoundException> { call, cause ->
            call.respond(HttpStatusCode.NotFound, ApiError(cause.message ?: "Not found", 404))
        }
        exception<ValidationException> { call, cause ->
            call.respond(HttpStatusCode.BadRequest, ApiError(cause.message ?: "Invalid", 400))
        }
        exception<Throwable> { call, cause ->
            call.respond(
                HttpStatusCode.InternalServerError,
                ApiError("Internal error: ${cause.message}", 500)
            )
        }
    }
}

fun Application.configureRouting() {
    val todos = mutableListOf(
        TodoItem(1, "Learn Kotlin", true),
        TodoItem(2, "Build Ktor app", false),
        TodoItem(3, "Deploy to production", false)
    )
    var nextId = 4

    routing {
        // Health check
        get("/health") {
            call.respond(mapOf("status" to "ok", "timestamp" to System.currentTimeMillis()))
        }

        // REST API routes
        route("/api/todos") {
            // GET /api/todos
            get {
                val done = call.request.queryParameters["done"]?.toBoolean()
                val filtered = if (done != null) todos.filter { it.done == done } else todos
                call.respond(filtered)
            }

            // GET /api/todos/{id}
            get("/{id}") {
                val id = call.parameters["id"]?.toIntOrNull()
                    ?: throw ValidationException("Invalid ID")
                val todo = todos.find { it.id == id }
                    ?: throw NotFoundException("Todo $id not found")
                call.respond(todo)
            }

            // POST /api/todos
            post {
                val body = call.receive<CreateTodo>()
                if (body.title.isBlank()) throw ValidationException("Title required")
                val todo = TodoItem(nextId++, body.title)
                todos.add(todo)
                call.respond(HttpStatusCode.Created, todo)
            }

            // PUT /api/todos/{id}/toggle
            put("/{id}/toggle") {
                val id = call.parameters["id"]?.toIntOrNull()
                    ?: throw ValidationException("Invalid ID")
                val index = todos.indexOfFirst { it.id == id }
                if (index == -1) throw NotFoundException("Todo $id not found")
                todos[index] = todos[index].copy(done = !todos[index].done)
                call.respond(todos[index])
            }

            // DELETE /api/todos/{id}
            delete("/{id}") {
                val id = call.parameters["id"]?.toIntOrNull()
                    ?: throw ValidationException("Invalid ID")
                if (!todos.removeIf { it.id == id }) throw NotFoundException("Todo $id not found")
                call.respond(HttpStatusCode.NoContent)
            }
        }
    }
}

fun main() {
    embeddedServer(Netty, port = 8080) {
        configurePlugins()
        configureRouting()
    }.start(wait = true)
}

Sponsored

Deploy on Railway

Use Cases

  • REST API server with Ktor
  • Structured error handling middleware
  • Content negotiation and serialization

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.