skip to content

What does Ktor's Resources plugin change about how routes are declared and handled?

level: middleimportance: nice to knowfreq 28%

answer

  1. Routes become classes, not strings
  2. Parameters arrive already typed
  3. Rename a property and the build breaks
  4. URLs can be generated from an instance
  5. Needs the serialization compiler plugin

basics

~20 s

It replaces string path templates with serializable classes: install Resources, annotate a class with @Resource("/articles"), then handle it with get<Articles> { articles -> }. Path and query values arrive as typed properties, and href builds URLs from an instance.

solid answer

~40 s

With the Resources plugin you declare each route as a `@Serializable` class annotated `@Resource("/articles")`, whose constructor properties are the route's path and query parameters. After `install(Resources)` you write `get<Articles> { articles -> }` and the handler receives a populated instance instead of a `Parameters` map, so `articles.sort` is already the declared type and the compiler catches a renamed parameter. Nesting is expressed by nested classes that hold a `parent` property, which composes the paths. The second half of the value is URL construction: `href(Articles(sort = "new"))` produces the path and query string for that instance, so links and redirects stop being hand-concatenated strings. It needs the kotlinx.serialization compiler plugin and the resources artifact, and the same route classes can be shared with the Ktor client to type calls from the other side.

code

kotlin · 15 lines
kotlin
@Serializable
@Resource("/articles")
class Articles(val sort: String? = "new") {
    @Serializable
    @Resource("{id}")
    class Id(val parent: Articles = Articles(), val id: Long)
}

fun Application.module() {
    install(Resources)
    routing {
        get<Articles> { articles -> call.respondText("sorted by ${articles.sort}") }
        get<Articles.Id> { res -> call.respondText("article ${res.id}") }
    }
}

go deeper

for a junior

Recognise the shape: an annotated class per route, a generic get<T> handler, and typed properties instead of a parameters map. Recalling that it is opt-in is enough here.

for a middle

Explain the mechanics — the annotation carries the template, constructor properties bind path and query values, nesting composes via a parent property — and what href does.

for a senior

Argue the tradeoff honestly: compile-time coupling and generated URLs versus the serialization plugin and extra ceremony, plus when the string DSL is the better call.

for a principal

Own the adoption decision across services: whether route classes become a shared contract with Kotlin clients, and the migration cost of standardising one style.

## The problem it solves The default routing DSL identifies routes by strings and hands you a `Parameters` multimap. That is flexible and readable, but it has two soft spots. First, the link between the template and the reads is textual: rename `{id}` to `{userId}` in the path and `call.parameters["id"]` compiles fine and returns null at runtime. Second, every URL you build elsewhere — a redirect, a `Location` header, a link in a response body — is string concatenation that no one checks against the route that will receive it. ## Declaring resources The Resources plugin turns routes into types. Add the resources artifact and the kotlinx.serialization compiler plugin, then `install(Resources)` in your module. A route becomes a class: `@Serializable @Resource("/articles") class Articles(val sort: String? = "new")` The annotation carries the path template. Constructor properties that appear in the template are bound from the path; the rest are bound from the query string. Nullability and defaults describe optionality, which is why the type declaration is the whole contract — there is no separate validation step for presence. Nesting composes through a parent property: `@Resource("{id}") class Id(val parent: Articles = Articles(), val id: Long)` declared inside `Articles` yields `/articles/{id}`, with `id` typed as `Long`. A request whose id is not numeric fails to deserialize into the class, which is the typed equivalent of the manual `toLongOrNull()` branch you would otherwise write. ## Handling and building Handlers are the generic overloads of the familiar verbs: `get<Articles> { articles -> call.respond(repo.list(articles.sort)) }` The lambda parameter is the populated instance. Rename a property and every handler that reads it stops compiling — the failure moves from runtime to build time, which is the main argument for the plugin. The reverse direction is `href`: `href(Articles.Id(id = 42))` renders the path for that instance, query parameters included. Redirects, hypermedia links and tests stop hardcoding paths, so changing a route's template updates every URL that points at it. ## Sharing with the client The same annotated classes work with Ktor's client-side resources support, so a Kotlin consumer can issue a request from a route instance rather than a string. In a codebase where both sides are Kotlin and can share a module, that turns the route table into a compile-checked contract between them. That is the strongest case for adopting the plugin and the reason it shows up mostly in Kotlin-multiplatform-shaped projects. ## Costs and when to skip it It is not free. You take on the serialization compiler plugin, an extra artifact, and a declaration style that is heavier than a one-line `get("/health")`. Route classes add indirection for anyone reading the code who does not know the plugin, and unusual path shapes are more awkward to express than in a raw template. For a small service, or one whose consumers are not Kotlin, the string DSL with disciplined parameter parsing is a perfectly defensible choice — and mixing both in one application is legal, since resource routes and string routes live in the same tree. ## Lineage The idea predates the current plugin: Ktor 1.x shipped an experimental Locations feature using an `@Location` annotation with the same intent. Resources is its successor, built on kotlinx.serialization rather than reflection. If you meet `@Location` in an existing codebase, you are looking at the older mechanism, and the migration is mostly mechanical — annotate the classes `@Serializable`, swap the annotation, and install the new plugin. ## What interviewers listen for The question is usually a differentiator rather than a gate, so the answer they want is a comparison, not a tutorial: what the plugin buys (typed parameters, compile-time coupling between routes and handlers, generated URLs), what it costs (serialization plugin, extra ceremony), and a clear-headed statement of when you would not bother. A candidate who presents it as strictly better than the string DSL has usually not shipped with it.

  • How are nested paths expressed with Ktor's Resources plugin?
    By nesting the classes and giving the inner one a property referring to the outer resource. An inner `@Resource("{id}")` class inside `@Resource("/articles")` composes to `/articles/{id}`, and the parent's own parameters remain available on the instance the handler receives.
  • What does href give you that a string path does not?
    It renders a URL from a route instance, so links, redirects and tests derive their paths from the same declaration that serves them. Change a route's template and every generated URL follows; a hand-concatenated string would silently keep pointing at the old path.
  • When would you stay with the string routing DSL instead?
    When the service is small, the consumers are not Kotlin, or the team would rather not add the serialization compiler plugin and the extra declaration ceremony. Disciplined parameter parsing in the string DSL is a defensible design; the plugin earns its keep mostly where route classes can be shared with a Kotlin client.

saying these in an interview costs you the question

  • Thinks the Resources plugin scans the classpath for handlers
  • Forgets the route class must also be @Serializable
  • Believes it replaces the string routing DSL rather than coexisting with it
  • Confuses it with the deprecated @Location Locations feature
  • Claims it validates business rules, not just parameter shapes

context