skip to content

Ktor

A Kotlin-first asynchronous framework where the server is a coroutine-based pipeline assembled from plugins, with a matching HTTP client. Interviewers ask about it in Kotlin shops as the lightweight alternative to Spring Boot.

on this pageshow

explore

questions

29

In Ktor, what is the difference between starting a server with embeddedServer and with EngineMain?

level: juniorimportance: must knowfreq 80%

answer

  1. Two ways to start: code or file
  2. One reads a config file at boot
  3. HOCON keys under ktor.deployment / ktor.application
  4. Module referenced by fully-qualified name

basics

~10 s

embeddedServer starts Ktor from Kotlin code, with the engine, host and port passed as arguments. EngineMain is a prebuilt main function that boots the same server from an application.conf or application.yaml file instead.

solid answer

~40 s

Both produce the same running Ktor server; they differ in where the configuration lives. `embeddedServer(Netty, port = 8080) { module() }.start(wait = true)` is *code-based* configuration: you pick the engine factory, port and host as Kotlin arguments and call your module directly. `EngineMain` is a `main` function shipped by each engine artifact — you set the application's main class to `io.ktor.server.netty.EngineMain` and it reads a config file (`application.conf` in HOCON, or `application.yaml`), taking `ktor.deployment.port` and the `ktor.application.modules` list from there. Code-based setup is the natural fit for tests and small tools, because it can be started and stopped inline on any port. File-based setup is the deployment default: ops can change the port or swap modules without recompiling, and the same jar is reconfigurable per environment.

code

kotlin · 11 lines
kotlin
fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        module()
    }.start(wait = true)
}

fun Application.module() {
    routing {
        get("/health") { call.respondText("OK") }
    }
}

go deeper

for a junior

Recall both entry points by name and be able to write the embeddedServer one-liner from memory, including the engine argument and the port. Know that application.conf is HOCON.

for a middle

Explain that the module list is resolved reflectively by fully-qualified JVM name at startup, and why that means a typo fails at boot instead of at compile time.

for a senior

Argue the deployment consequence: file-based config lets one artifact be repointed per environment, so pick EngineMain for services and code-based startup for tests and tools.

for a principal

Own the convention across services — one main class, one config layout, modules split so that environments can enable subsets — so that operability does not vary team by team.

## The two entry points A Ktor server is an engine plus one or more *modules*. Ktor gives you two ways to assemble them, and interviewers ask about the difference because it decides how the service is configured in production. ### embeddedServer — configuration in code ``` fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0") { module() }.start(wait = true) } ``` `embeddedServer` is a function that takes an *engine factory* (`Netty`, `CIO`, `Jetty`, `Tomcat`) as its first argument, some connector settings, and a lambda that runs with `Application` as its receiver. It returns a server object you can `start()` and `stop()`. `wait = true` blocks the calling thread until the server stops; `wait = false` returns immediately, which is what tests want so they can issue requests and then shut the server down. Everything is visible in Kotlin: the port is a literal or a variable, the module is a direct function call, and nothing is resolved by name at runtime. That makes it refactor-safe and IDE-navigable. ### EngineMain — configuration in a file Each server engine artifact ships an object called `EngineMain` with a `main(args)` function. You point your build at it: ``` application { mainClass.set("io.ktor.server.netty.EngineMain") } ``` and put the settings in `src/main/resources/application.conf`: ``` ktor { deployment { port = 8080 } application { modules = [ com.example.ApplicationKt.module ] } } ``` The format is HOCON (Human-Optimized Config Object Notation), the Typesafe Config format. Ktor also supports `application.yaml` when the YAML config artifact is on the classpath. `EngineMain` loads that file, builds the engine, and then resolves every entry in `ktor.application.modules` **by fully-qualified name** and invokes it against the `Application`. That name is the part people get wrong. A top-level function `fun Application.module()` declared in `Application.kt` compiles to a static method on the class `ApplicationKt`, so the reference is `com.example.ApplicationKt.module`. Misspell it and the server fails at startup with a class-not-found style error rather than a compile error — the cost of late binding. ## Why the choice matters **Reconfiguration without a rebuild.** With `EngineMain` the port, the connector, the log level and even which modules are enabled are data. A container image can be reused across environments and driven by config. With `embeddedServer` the same change is a code change. **Environment overrides.** File-based setup composes naturally with HOCON substitution such as `port = ${?PORT}`, which takes the value from an environment variable if it is set and leaves the previous value otherwise — exactly the shape a PaaS or Kubernetes deployment wants. **Tests.** Test code overwhelmingly uses code-based startup, directly or through Ktor's test host, because it needs an ephemeral port and an in-process lifecycle. A common production layout therefore ships `EngineMain` for the real deployment while tests call the same `Application.module()` function through `embeddedServer` or the testing DSL. Because a module is just an extension function on `Application`, both paths reuse it unchanged. **Hybrid.** The two are not exclusive. You can start `embeddedServer` and still read `application.conf`, since the config is available through the application environment; conversely `EngineMain` accepts command-line arguments that override file values. ## What a good answer includes Say that both end up at the same `Application` object, name HOCON/`application.conf` as what `EngineMain` reads, mention `ktor.deployment.port` and `ktor.application.modules`, and give the practical rule: file-based for deployment flexibility, code-based for tests and single-purpose tools. Note the tradeoff honestly — the config file buys you runtime flexibility and costs you compile-time checking of module references.

  • Why does a module reference in application.conf look like com.example.ApplicationKt.module rather than com.example.module?
    A Kotlin top-level function is compiled into a synthetic class named after its file — `Application.kt` becomes `ApplicationKt` — and the module is a static method on it. Ktor resolves the string reflectively, so it needs the JVM name, not the Kotlin source name. Renaming the file silently breaks startup.
  • How would you start a Ktor server on a random free port inside a test?
    Use code-based startup with port 0 and `wait = false`, then read the actually bound port back from the engine's resolved connectors before issuing requests. Ktor's own testing support does the equivalent for you, which is why tests almost never go through EngineMain.
  • Can one application.conf list more than one module?
    Yes. `ktor.application.modules` is a list, and Ktor invokes each entry against the same `Application` in order. That is how teams split wiring into, say, a routing module, an observability module and a persistence module, and how a config file can enable or disable one of them per environment.

saying these in an interview costs you the question

  • Claims EngineMain is faster or uses a different engine
  • Thinks embeddedServer cannot read application.conf at all
  • States the modules entry is a Kotlin function reference checked at compile time
  • Says wait = true is required for the server to serve requests
  • Confuses application.conf with a Spring application.properties equivalent

context

open as a page

In Ktor, how do you protect a route with the Authentication plugin and read the principal?

level: juniorimportance: must knowfreq 76%

basics

~10 s

Install Authentication and configure a named provider, then wrap the routes in authenticate("name") { }. Inside a handler, call.principal<T>() returns whatever the provider's validate block produced; routes outside the block stay public.

open as a page

In Ktor, how do you create an HttpClient and read the status and body of a GET response?

level: juniorimportance: must knowfreq 78%

basics

~10 s

Construct an HttpClient with an engine, then call the suspend function client.get(url). It returns an HttpResponse: read the code from response.status and the payload from response.bodyAsText() or the typed response.body<T>().

open as a page

In Ktor, why do call.receive<User>() and call.respond(user) fail unless ContentNegotiation is installed?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Ktor's pipelines natively handle only bytes, text and form data. ContentNegotiation registers converters that map a media type to a class, so without it no converter exists for User and both the receive and the respond transformation fail.

open as a page

In Ktor, what does install() do, and at which scopes can a plugin be installed?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Ktor's install() adds a plugin to a pipeline and runs its configuration block once at startup. Plugins can be installed application-wide inside a module, or — when the plugin is route-scoped — on a single route so only that subtree is affected.

open as a page

In Ktor, how do you declare a route with a path parameter and read path and query values?

level: juniorimportance: must knowfreq 80%

basics

~10 s

Put 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.

open as a page

In Ktor's jwt auth provider, what do the verifier and validate blocks each do?

level: middleimportance: must knowfreq 66%

basics

~20 s

In Ktor's jwt provider, verifier supplies the token verifier that checks the signature and standard claims before your code runs; validate then receives the already-verified JWTCredential and decides, in application terms, whether to return a principal or null.

open as a page

Why does a Ktor client call to body<User>() fail without ContentNegotiation installed?

level: middleimportance: must knowfreq 58%

basics

~20 s

Ktor's client core only converts payloads to bytes, text and channels. Typed conversion needs a converter, supplied by the client ContentNegotiation plugin; without it body<User>() throws NoTransformationFoundException because no registered converter can produce that type.

open as a page

What are the phases of Ktor's ApplicationCallPipeline, and what determines the order interceptors run in?

level: middleimportance: must knowfreq 58%

basics

~20 s

Ktor's ApplicationCallPipeline has the phases Setup, Monitoring, Plugins, Call and Fallback, executed in that order. Within one phase, interceptors run in the order they were registered, so both the phase and the install order decide placement.

open as a page

When several Ktor routes match the same request path, how does Ktor choose the handler?

level: middleimportance: must knowfreq 55%

basics

~20 s

Ktor builds a routing tree, evaluates every branch that matches, scores each one, and runs the highest-scoring handler. A constant segment outranks a path parameter, which outranks a wildcard, which outranks a tailcard. Declaration order is not the tiebreaker.

open as a page

In Ktor, authenticate {} proves identity — how do you enforce role checks per route?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Ktor ships no permission model, so authorization is code you add inside the authenticated subtree: either an explicit check in the handler, or a route-scoped plugin that hooks AuthenticationChecked, reads the principal's roles and responds 403 before the handler runs.

open as a page

Why should a Ktor HttpClient be created once and reused instead of per request?

level: seniorimportance: must knowfreq 55%

basics

~20 s

A Ktor HttpClient owns an engine with threads, connections and plugin state such as cookie storage and cached tokens. Creating one per request multiplies those resources, discards warm connections and state, and leaks unless every instance is closed.

open as a page

How do you choose between Ktor's Netty, CIO and Jetty server engines for a service?

level: middleimportance: should knowfreq 48%

basics

~20 s

Netty is the default and the safest production choice on the JVM. CIO is Ktor's own pure-Kotlin engine, the one that works on non-JVM targets and keeps dependencies minimal. Jetty and Tomcat exist mainly for servlet-container compatibility.

open as a page

In Ktor, how do you read a custom value from application.conf inside a module function?

level: middleimportance: should knowfreq 58%

basics

~10 s

Go through the application environment's config object: from an Application receiver, call environment.config.property("path.to.key").getString(), or propertyOrNull for an optional value. The config is the parsed application.conf tree, so custom keys live alongside Ktor's own.

open as a page

In Ktor, how does session-based authentication work with the Sessions plugin?

level: middleimportance: should knowfreq 54%

basics

~10 s

Two plugins cooperate: Sessions defines a typed session carried in a cookie, and Authentication's session provider validates it per request. A login route calls call.sessions.set(...); protected routes wrap in authenticate and read the principal.

open as a page

What is a Ktor client engine, and how do you choose one for JVM, Android, or iOS?

level: middleimportance: should knowfreq 52%

basics

~20 s

A Ktor client engine is the pluggable implementation that actually performs requests. HttpClient is the shared API; the engine comes from a separate artifact passed as HttpClient(CIO), HttpClient(OkHttp), HttpClient(Darwin), and differs per platform and capability.

open as a page

In the Ktor client, what does expectSuccess change, and how do you handle a 404?

level: middleimportance: should knowfreq 46%

basics

~10 s

Ktor's client sets expectSuccess to false by default, so a 404 returns a normal HttpResponse you must inspect via response.status. Setting expectSuccess = true makes non-2xx responses throw ClientRequestException, ServerResponseException or RedirectResponseException instead.

open as a page

In Ktor's ContentNegotiation, how do the json() and jackson() converters differ?

level: middleimportance: should knowfreq 58%

basics

~20 s

Ktor's json() uses kotlinx.serialization: a compiler plugin generates serializers at build time for @Serializable types. jackson() wraps a Jackson ObjectMapper that works by runtime reflection on any class, annotated or not. Compile-time safety versus reflective flexibility.

open as a page

In Ktor, why does a file upload use call.receiveMultipart() rather than receive<T>()?

level: middleimportance: should knowfreq 52%

basics

~20 s

A multipart/form-data body is a stream of independently typed parts, not one document a converter can turn into a single object. Ktor exposes it as MultiPartData, iterated with forEachPart, so large files stream to disk instead of loading into memory.

open as a page

How do you write a custom Ktor plugin, and when should it be route-scoped instead of application-scoped?

level: middleimportance: should knowfreq 46%

basics

~20 s

Build it with createApplicationPlugin, giving a name and handlers such as onCall, onCallReceive and onCallRespond; add a configuration class if it needs settings. Use createRouteScopedPlugin instead when the behaviour should apply to one route subtree rather than the whole server.

open as a page

How do you serve static files and packaged resources from a Ktor server's routing block?

level: middleimportance: should knowfreq 42%

basics

~20 s

Inside Ktor's routing block, call staticResources("/assets", "assets") to serve files packaged on the classpath, or staticFiles("/files", File("files")) to serve a directory on disk. Both take a URL prefix, a base location, and an optional index file.

open as a page

Your Ktor service must take its port and credentials from the environment in production. How do you wire that?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Keep one config file that defines the shape and bind sensitive or environment-specific values to HOCON substitutions such as port = ${?PORT}. EngineMain also accepts command-line overrides, so the deploy can set values without a rebuild and the code still reads a single config tree.

open as a page

In Ktor, what does an authentication provider's challenge block control?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The challenge block defines the response sent when no provider authenticated the caller — the default 401 with a WWW-Authenticate header for basic, or whatever you write: a JSON error body for an API, a redirect to a login page for a browser application.

open as a page

In Ktor, how do you turn a body that fails receive<T>() into a 400 with a JSON error?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Install StatusPages alongside ContentNegotiation and register exception handlers for the conversion failures — Ktor's BadRequestException and the converter's own convert exception — responding with a serializable error DTO. Unhandled, those exceptions escape the handler and become 500s.

open as a page

A Ktor endpoint returns responses without the CORS headers you configured. How do you diagnose it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Check three things in order: that CORS is installed on a pipeline the request actually passes through, that the configuration matches the browser's origin, method and requested headers exactly, and that nothing responded and finished the pipeline before the plugin ran.

open as a page

How would you organize a Ktor service's routing once it spans dozens of endpoints across features?

level: seniorimportance: should knowfreq 47%

basics

~20 s

Split the tree into Route extension functions, one per feature — fun Route.orderRoutes(service: OrderService) — living beside that feature's code, and call them from a single routing block that owns shared prefixes. Handlers stay thin: parse, delegate, respond.

open as a page

In the Ktor client, how do you open a WebSocket connection and exchange frames?

level: middleimportance: nice to knowfreq 32%

basics

~10 s

Install the WebSockets plugin on the HttpClient, then call client.webSocket("wss://host/path") { ... }. Inside the block you send with send(Frame.Text(...)) and read from incoming; the session closes when the block returns.

open as a page

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

level: middleimportance: nice to knowfreq 28%

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.

open as a page

In Ktor, how do you serve a custom media type such as application/vnd.api+json?

level: seniorimportance: nice to knowfreq 28%

basics

~10 s

Register a converter against that exact ContentType inside install(ContentNegotiation). Ktor's json() and jackson() both accept a contentType parameter, and register(contentType, converter) binds any ContentConverter to any media type, including several in one application.

open as a page