In Ktor, what does install() do, and at which scopes can a plugin be installed?
answer
- Nothing cross-cutting is enabled by default
- The trailing lambda runs at startup, not per call
- One instance per scope, per key
- A subtree can have its own policy
basics
~20 sKtor'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.
solid answer
~40 sIn Ktor almost every cross-cutting capability is a plugin, and `install()` is how you attach one. Called on the `Application` inside a module function, `install(Compression) { gzip() }` registers the plugin for the whole server: the trailing lambda is its configuration block, evaluated **once during startup**, not per request. What the plugin then does is subscribe to points in the call pipeline, so every subsequent request passes through it. Some plugins are route-scoped and can also be installed inside a `route("/admin") { install(...) }` block, limiting their effect to that subtree — useful when only part of the API needs, say, a stricter policy. Installing the same plugin twice in the same scope is an error, not a merge, so configuration for one plugin belongs in a single `install` call.
code
kotlin · 11 linesfun Application.module() {
install(Compression) {
gzip()
}
install(CachingHeaders) {
options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 60)) }
}
routing {
get("/health") { call.respondText("OK") }
}
}go deeper
Recall the install(Plugin) { config } shape, that it lives inside a module function, and that the block is startup configuration. Be able to name a few built-in plugins.
Explain that one configured instance exists per scope, why the config block therefore runs once, and what route scope buys over application scope.
Decide deliberately what belongs globally versus per subtree, and keep the module readable by grouping installs into extension functions with clear ownership.
Own the platform baseline — which plugins every service installs and how they are configured — so cross-cutting behaviour is consistent instead of copied from whichever template a team started from.
## Plugins are the extension model Ktor's core is deliberately small: an engine, a call pipeline, and routing. Compression, CORS, caching headers, content negotiation, call logging, status pages, request validation — all of it is a *plugin* you opt into. Nothing is on by default, which is why a fresh Ktor app does not, for example, negotiate JSON until you install the plugin that does. `install()` is the single verb for that opt-in: ``` fun Application.module() { install(Compression) { gzip() } install(CORS) { allowHost("example.com", schemes = listOf("https")) allowHeader(HttpHeaders.ContentType) } routing { /* ... */ } } ``` The first argument is the plugin object itself; the trailing lambda is a **configuration block** whose receiver is that plugin's own config type. That block runs exactly once, during application startup. This trips people up: code inside `install(X) { ... }` is setup, not per-request logic. If you want per-request behaviour you either configure the plugin to do it or write a custom plugin whose handlers fire per call. ## What installation actually does Installing stores the configured plugin under a key on the pipeline and lets the plugin register its own handlers at points in the call lifecycle. From then on, each incoming call flows through those handlers before or after reaching your route handler. The plugin is a *singleton per scope*, which is why the config block is evaluated once — the resulting configuration is shared by every request. That also explains why installing the same plugin twice in the same scope is rejected rather than silently merged: the pipeline holds one instance per key, and two `install` calls would leave ambiguity about which configuration wins. Consolidate into one call. ## Scopes **Application scope.** `install(...)` on the `Application` receiver inside a module function. Applies to every call the server handles. This is the common case and where most plugins belong. **Route scope.** Plugins built as route-scoped can be installed inside a `route` block: ``` routing { get("/public") { call.respondText("open") } route("/admin") { install(AdminOnlyPlugin) get("/stats") { call.respondText("secret") } } } ``` Only calls that match that subtree see the plugin. This matters when a policy is genuinely local — an audit log for a sensitive subtree, a stricter payload limit for an upload endpoint — and it keeps the cost off the routes that do not need it. The distinction is a property of how the plugin was written, not a flag at the call site: a plugin authored for application scope cannot simply be dropped into a route block. ## Where install calls live Because a module is just `fun Application.module()`, installation is ordinary Kotlin code and can be factored: ``` fun Application.module() { configureMonitoring() configureSerialization() configureRouting() } fun Application.configureMonitoring() { install(CallLogging) } ``` Those helper functions are extension functions on `Application` as well, so each one can install what it owns. This is the layout Ktor's own project generator produces, and it is the reason a Ktor app's setup stays readable as the plugin list grows. ## Common built-in plugins worth naming - **CORS** — browser cross-origin policy: which hosts, methods and headers are allowed. - **Compression** — response encoding, e.g. `gzip()` and `deflate()`, with conditions such as a minimum size. - **CachingHeaders** — attaches `Cache-Control` and related headers to outgoing content. - **CallLogging** — logs each call. - **ContentNegotiation** — request/response body conversion. - **StatusPages** — maps exceptions and status codes to responses. Being able to name a handful, and to say what each *plugs into*, is the tell that you understand the model rather than having copied a template. ## The one-sentence summary for an interview "`install()` attaches a plugin to a pipeline and runs its configuration once at startup; the plugin then hooks into every call flowing through that pipeline. Application scope covers the whole server, route scope narrows it to a subtree, and a plugin only supports route scope if it was written for it."
- Why is code inside install(X) { ... } not executed per request?The lambda configures the plugin instance, and there is one instance per scope created during startup. Per-request work happens in the handlers the plugin registers on the pipeline, so putting request-specific logic in the config block runs it exactly once, against no call at all.
- What is the practical reason to install a plugin on a route rather than the whole application?Scope of effect and cost. A policy that only makes sense for one subtree — extra auditing, a tighter body limit — should not run for every request. It also documents intent: reading the route block tells you what applies there, instead of a global list you must mentally filter.
- What happens if a plugin's configuration is split across two install calls for the same plugin in one scope?It is rejected rather than merged. The pipeline holds a single configured instance under the plugin's key, so a second installation in the same scope is a startup error. The fix is to consolidate the configuration into one call.
saying these in an interview costs you the question
- Thinks the install configuration block runs on every request
- Expects two install calls for one plugin to merge their settings
- Assumes plugins like CORS or Compression are enabled by default
- Believes any plugin can be installed inside a route block
- Describes install() as registering a route handler