In Ktor, how does `routing { route("/api") { get("/users") { } } }` build a nested route tree? Describe the receiver types at each level and why nesting works.
answer
- routing -> Routing : Route (root node)
- route() makes a child node, runs lambda on it
- get/post lambda receiver is RoutingContext, has call
- Structural lambdas Route.() -> Unit, handler is suspend
- Builders compose into a route tree
basics
~10 sEach block is a lambda whose this is a route node. route("/api") makes a child node and runs the inner lambda against it, so get inside attaches to /api, building a tree of routes.
solid answer
~40 s`routing { }` opens a lambda with receiver type `Routing` (which extends `Route`). Builder functions like `route`, `get`, `post`, and `install`-adjacent helpers are extension functions on `Route`. `route("/api") { }` creates a child Route node under the current node and invokes its trailing lambda `Route.() -> Unit` with the child as `this`; therefore `get("/users") { }` inside attaches a GET handler to `/api/users`. Nesting works because every builder function takes a `Route.() -> Unit` receiver lambda and the receiver is always the current node, so children compose into a tree. The innermost `get { }` lambda has a different receiver — `RoutingContext` (formerly `PipelineContext`) — exposing `call`. This is the routing-DSL idiom: receiver-lambda builders that mutate a shared tree structure rather than a flat settings object.
code
kotlin · 13 linesfun Application.module() {
install(ContentNegotiation) { json() } // plugin config DSL
routing { // this: Routing
route("/api") { // this: Route (/api)
get("/users") { // this: RoutingContext
call.respondText("users")
}
authenticate("jwt") { // nested guard, still Route
post("/users") { call.respond(HttpStatusCode.Created) }
}
}
}
}go deeper
Knows nesting routes builds nested paths and get adds an endpoint.
Names the receiver types (Routing/Route vs RoutingContext) and explains tree construction via child nodes.
Distinguishes structural builder lambdas from the suspend handler lambda and explains install vs route idioms.
Reasons about DSL design: deferred registration, matching/dispatch at request time, and how plugin config composes with routing.
## Ktor routing as a tree-building DSL Ktor's routing DSL builds a **tree of Route nodes**. Each path segment or method handler is a node; the DSL nests by rebinding `this` to deeper nodes. ### Receiver types, level by level - `routing { ... }` — receiver is **`Routing`**, the root route node. `Routing` extends `Route`. - `route("/api") { ... }` — `route` is an extension `fun Route.route(path: String, build: Route.() -> Unit): Route`. It creates a **child Route** for `/api` and runs `build` with that child as `this`. - `get("/users") { ... }` — `get` is `fun Route.get(path: String, body: suspend RoutingContext.() -> Unit)`. It registers a child route for the path plus the GET method, and its lambda's receiver is **`RoutingContext`** (older Ktor: `PipelineContext<Unit, ApplicationCall>`), exposing `call`. ```kotlin routing { // this: Routing (a Route) route("/api") { // this: Route for /api (child of root) get("/users") { // this: RoutingContext; registered at /api/users call.respondText("users") } post("/users") { call.respond(HttpStatusCode.Created) } } } ``` ### Why nesting composes into a tree Every structural builder (`route`, `get`, `authenticate`, etc.) shares the contract: take a `Route.() -> Unit` lambda and call it against the **newly created child node**. Because `this` is always the current node, builders called inside automatically attach to the right parent. The final tree is what Ktor walks at request time to match a URL. ### `install` blocks — a different but related idiom `install(ContentNegotiation) { json(Json { prettyPrint = true }) }` is the **plugin/feature** config DSL: `install` takes a plugin and a configuration lambda whose receiver is that plugin's Configuration class. Same receiver-lambda-over-settings pattern, but it configures a pipeline plugin rather than adding route nodes. ### Suspending handler lambdas Note the handler lambda is `suspend RoutingContext.() -> Unit` — it can call `call.receive()` / `call.respond()` which suspend. The structural `route { }`/`get(path) { }` builder lambdas are **not** suspend; only the request-handling body is. ### Key APIs/keywords - `Route.() -> Unit` builder lambdas (structural) - `suspend RoutingContext.() -> Unit` handler lambda - `route`, `get`, `post` extension functions on `Route` - `Routing : Route` root node - `install(Plugin) { ... }` configuration lambda - `call` available via the RoutingContext receiver
- Why can you call `call.respond(...)` inside a `get { }` body but not inside the surrounding `route { }` lambda?The `get` handler lambda has receiver RoutingContext which exposes `call`; the structural `route` lambda's receiver is a Route node, which has no `call`.
- Is the `get("/users") { }` body suspending? Why does that matter?Yes, it is `suspend RoutingContext.() -> Unit`, so it can call suspending I/O like call.receive()/respond() without blocking a thread.
Like building a folder tree: each route opens a subfolder and everything you create inside lands in it; get drops a file in the current folder.
saying these in an interview costs you the question
- Claiming all the lambdas share the same receiver type
- Thinking routing executes the handler immediately rather than registering it for later
- Confusing install (plugin config) with route (tree building)
- Saying `call` is available everywhere inside routing { }
- Believing the structural route/get builder lambdas are suspend