In Ktor, how do you declare a route with a path parameter and read path and query values?
answer
- Braces in the path capture a segment
- Everything you read back is a nullable String
- Query values sit on the request object
- One container holds repeated values
- Conversion failures are yours to answer
basics
~10 sPut the parameter in the path template — get("/users/{id}") — and read it with call.parameters["id"]. Query string values come from call.request.queryParameters. Both return nullable strings, so you convert and validate them yourself.
solid answer
~40 sRoutes are declared inside a `routing { }` block. A path parameter is a brace-wrapped segment: `get("/users/{id}") { ... }`, and inside the handler `call.parameters["id"]` returns the captured segment as a `String?`. Query parameters come from `call.request.queryParameters["page"]`; that container is a multimap, so `getAll("tag")` returns every repeated value while `[]` returns only the first. Nothing is typed or validated for you — you convert with `toIntOrNull()` and choose the failure response, or use `call.parameters.getOrFail<Int>("id")` from `io.ktor.server.util`, which throws a `BadRequestException` subtype that Ktor answers with 400. One trap worth naming: inside a routing handler `call.parameters` merges path parameters with the query string, so read `call.request.queryParameters` when you specifically mean the query.
code
kotlin · 9 linesrouting {
get("/users/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, "id must be numeric")
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
val tags = call.request.queryParameters.getAll("tag").orEmpty()
call.respondText("user=$id page=$page tags=$tags")
}
}go deeper
Be ready to write a three-route Ktor block from memory: a literal path, a path with {id}, and one that reads a query parameter. Say out loud that both reads return String?.
Explain the mechanics: parameters are a multimap, path and query are merged in call.parameters inside a handler, and conversion plus the 400 response are the handler's responsibility.
Show a convention. Interviewers want to hear how you make parameter parsing and error bodies consistent across dozens of handlers rather than ad hoc per route.
Own the contract angle: which parameters are required, how strict the API is about unknown query keys, and whether typed route classes are worth adopting across teams.
## Where routing lives A Ktor server describes its URLs with a DSL rather than annotations. Inside an application module you open a `routing { }` block and nest selectors; the first use installs Ktor's `Routing` plugin. The smallest useful route pairs an HTTP method with a path: `get("/health") { call.respondText("OK") }`. The lambda runs with the call in scope, and everything below is about how values from the URL reach that lambda. ## Path parameters A brace-wrapped segment in the path template captures that segment by name: `get("/users/{id}") { val id = call.parameters["id"] }` `call.parameters["id"]` returns the captured text as a `String?` — percent-decoded, but otherwise raw. Ktor performs no type conversion here, and a name you never declared simply yields `null` rather than an error. The template supports a few shapes beyond the plain `{id}`: - `{id?}` marks the segment optional, so both `/users` and `/users/7` reach the handler. - `*` matches exactly one segment without capturing it. - `{tail...}` is a tailcard: it matches the remaining segments, including the slashes between them, and the pieces are read with `call.parameters.getAll("tail")`. A plain `{id}` can never contain a slash, because it is scoped to a single path segment. If you need to accept a value that includes slashes — a file path, a nested key — a tailcard is the mechanism. ## Query parameters Query string values live on the request: `call.request.queryParameters["page"]`. Both `Parameters` containers are multimaps built on Ktor's `StringValues`, which matters more often than people expect. For `/posts?tag=kotlin&tag=ktor`, `queryParameters["tag"]` returns only `"kotlin"`, while `queryParameters.getAll("tag")` returns `["kotlin", "ktor"]`. `names()` gives the set of keys, which is how you detect an unexpected parameter if your API is strict about them. ## The merge trap Inside a routing handler, `call.parameters` is not a path-only view: it exposes the captured path parameters merged with the query string parameters. That is convenient for small handlers, but it means a request to `/users/x?id=7` can produce a non-null `call.parameters["id"]` even though the path capture is `"x"`. When you specifically mean a query value, read `call.request.queryParameters`; when you specifically mean the path, keep the parameter names in your templates distinct from anything a client can pass in the query. ## Conversion and failure handling Because parameters are strings, converting them is your job, and how you fail is a design decision: - Manual: `val id = call.parameters["id"]?.toLongOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest)`. Explicit, verbose, and it lets you shape the error body. - `getOrFail`: `call.parameters.getOrFail<Long>("id")` from `io.ktor.server.util` throws `MissingRequestParameterException` when the name is absent and `ParameterConversionException` when the text does not convert. Both are `BadRequestException` subtypes, so Ktor answers 400 without you writing the branch. If you want a consistent JSON error body for those, map them in a `StatusPages` handler. Pick one convention per service. A codebase where half the handlers return a custom 400 body and half return Ktor's default plain-text 400 is a contract inconsistency reviewers will flag. ## Worked shape A typical read endpoint parses the identifier from the path, optional paging from the query, validates both, then delegates: `get("/users/{id}/orders")` reads `id` from `call.parameters`, `page` and `size` from `call.request.queryParameters`, clamps `size` to a maximum, calls a service, and responds. Keeping the parsing in the handler and the logic in a service is what makes routing files stay readable. ## What interviewers listen for They want to hear that parameters are nullable strings, that you decide the failure response, that query parameters can repeat, and that a path segment cannot contain a slash. Candidates who describe Ktor as automatically binding and validating typed parameters are usually importing habits from an annotation-driven framework; typed binding in Ktor is opt-in through the Resources plugin, not the default of the string-template DSL.
- What happens when the client omits a parameter your handler needs?`call.parameters["x"]` returns null — nothing throws. You either branch and respond with 400 yourself, or use `call.parameters.getOrFail("x")`, which raises `MissingRequestParameterException`, a `BadRequestException` subtype Ktor answers with 400. Choose one convention service-wide so clients see a consistent error shape.
- How do you read a query parameter the client can repeat, such as ?tag=a&tag=b?Use `call.request.queryParameters.getAll("tag")`, which returns `List<String>?` with every value. The indexing operator returns only the first value, so a handler written with `queryParameters["tag"]` silently ignores the rest — a common source of "my filter only applies one tag" bugs.
- How would you accept a value containing slashes, like a file path, in the URL?A `{name}` capture is scoped to one path segment and can never contain a slash. Declare a tailcard instead — `get("/files/{path...}")` — and reassemble the value from `call.parameters.getAll("path")`. Validate the result before touching the filesystem, since it is attacker-controlled.
saying these in an interview costs you the question
- Claims Ktor converts path parameters to typed values automatically
- Thinks a missing parameter throws rather than returning null
- Declares path parameters as :id instead of {id}
- Uses the indexing operator for a repeated query parameter
- Parses call.request.uri by hand instead of using queryParameters