skip to content

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

level: middleimportance: should knowfreq 42%

answer

  1. Two sources: inside the jar or on disk
  2. It is declared like any other route
  3. There is a default document for the root
  4. Compress once, not per request
  5. A proxy usually sits in front in production

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.

solid answer

~40 s

Ktor mounts static content as ordinary routes. `staticResources("/assets", "assets")` serves everything under the `assets` package on the classpath — the right choice for files packaged inside the jar. `staticFiles("/files", File("files"))` serves a directory on the deployed filesystem, which is what you want for content that changes without a redeploy. Both accept an `index` argument (defaulting to `index.html`) so a request for the mount root returns a page, and both take a configuration lambda where you can exclude paths, serve pre-compressed variants with `preCompressed(CompressedFileType.GZIP)`, or answer HEAD automatically via `enableAutoHeadResponse()`. Because these are just routes, a more specific dynamic route under the same prefix still wins. In production most teams still put a CDN or reverse proxy in front and keep the Ktor mount for local development and small deployments.

code

kotlin · 7 lines
kotlin
routing {
    staticResources("/assets", "assets") {
        preCompressed(CompressedFileType.GZIP)
        enableAutoHeadResponse()
    }
    staticFiles("/files", File("/var/data/files"), index = "index.html")
}

go deeper

for a junior

Know the two calls and what each serves from: staticResources for files packaged on the classpath, staticFiles for a directory on disk. Be able to write one inside routing.

for a middle

Explain the index file, the configuration options such as pre-compressed variants, and why a static mount coexists with dynamic routes under the same prefix.

for a senior

Bring deployment reality: working-directory dependence, cache headers and content-hashed filenames, and when the mount should be a development convenience with a CDN in front.

for a principal

Own the asset-delivery strategy — where the build output lives, who serves it in each environment, and how invalidation works when a release ships new bundles.

## Static content is a routing concern in Ktor There is no separate static-file subsystem in Ktor. You declare static mounts inside the same `routing { }` block as your API endpoints, and they take part in the same match-and-score resolution as everything else. That is worth internalising, because it explains most of the behaviour that surprises people. ## The two mounts `staticResources(remotePath, basePackage)` serves from the classpath. `staticResources("/assets", "assets")` maps `GET /assets/app.css` to the resource `assets/app.css` — typically `src/main/resources/assets/app.css`, which ends up inside the jar. Because the served set is bounded by what you packaged, this is the safest option and the one that survives being deployed as a single fat jar or container image. `staticFiles(remotePath, dir)` serves from the filesystem. `staticFiles("/files", File("files"))` maps `GET /files/report.pdf` to `files/report.pdf` relative to the process working directory. Use it when content is written after the build — uploads, generated reports, a mounted volume. Its correctness now depends on the runtime environment: a relative `File` path means the working directory of the deployed process, which is a classic "works locally, empty in the container" bug. ## Index files and single-page apps Both functions take an `index` parameter, `"index.html"` by default, so a request to the mount root or to a directory beneath it returns that file instead of a listing. For a single-page frontend, the harder requirement is that deep links such as `/app/settings` also return the shell document rather than a 404; that is a fallback question, and the mechanism is a low-specificity route under the same prefix — a tailcard branch that responds with the shell — rather than anything special to the static mount. ## The configuration lambda Both mounts accept a trailing configuration block. The options worth knowing: - `preCompressed(CompressedFileType.GZIP)` — if a sibling file with the matching extension exists, Ktor serves the pre-compressed variant with the right encoding header, so you compress once at build time instead of per request. `CompressedFileType.BROTLI` is the other value. - `enableAutoHeadResponse()` — answers HEAD for the mounted files without you declaring anything. - `exclude { ... }` — keeps matching files out of the served set; useful when a directory legitimately contains things you do not want public. - `modify { ... }` — adjusts the outgoing call for a served file, which is where you attach caching headers per file. ## Interaction with your API routes Since static mounts are routes, precedence applies normally: a static mount is effectively a low-specificity branch under its prefix, so a literal or parameterised route you declare under the same prefix still wins its own requests. This is what lets `/assets/manifest.json` be generated dynamically while everything else under `/assets` is served from disk. It also means a broad static mount at `/` does not swallow your API — but it is still worth mounting static content under a distinct prefix so the URL space stays legible. ## Caching and production Ktor will serve the bytes; it will not design your caching strategy. Long-lived immutable assets want content-hashed filenames and a far-future cache lifetime, while the HTML shell wants to be revalidated. In a real deployment the common shape is a CDN or reverse proxy holding the assets and the Ktor mount serving as the origin or as the development-time convenience. Serving large media through the application process is possible but rarely the design you want, because it ties asset bandwidth to the same JVM that answers your API. ## Version note Older Ktor code uses a different DSL: `static("/assets") { resources("assets") }`, with `files(...)`, `defaultResource(...)` and `default(...)` inside the block. That form was deprecated in Ktor 2.3 in favour of `staticResources` and `staticFiles`. When you read a tutorial or a Stack Overflow answer that uses `static { }`, it predates 2.3; write new code against the newer functions, and expect the older DSL when maintaining a 2.0–2.2 codebase. ## What interviewers listen for They want to hear the classpath-versus-filesystem distinction and the reason to prefer the classpath for packaged assets, awareness that these are ordinary routes, and a sentence of production judgment about caching or fronting with a CDN. Naming the deprecated `static { }` DSL as legacy is a small credibility signal that you have worked across Ktor versions.

  • Why prefer staticResources over staticFiles for a frontend bundle?
    The bundle is a build artefact, so packaging it on the classpath makes it travel with the jar or image and bounds the served set to what you shipped. `staticFiles` resolves against the deployed filesystem and the process working directory, which is environment-dependent and empty in a minimal container unless you mount something.
  • What does preCompressed(CompressedFileType.GZIP) actually do?
    When a request matches a static file, Ktor looks for a sibling pre-compressed variant and, if the client accepts that encoding, serves it with the matching Content-Encoding instead of compressing on the fly. You pay the compression cost once at build time rather than per request, which matters for large text assets.
  • How would you cache static assets served by Ktor?
    Give immutable assets content-hashed filenames so their URL changes when the content does, then attach a long cache lifetime to those responses — the static mount's modify block is where you set headers per file. Keep the HTML shell short-lived or revalidated, and let a CDN in front absorb the volume.

saying these in an interview costs you the question

  • Uses staticFiles with a relative path and is surprised it is empty in the container
  • Believes a static mount at / shadows the API routes
  • Thinks the deprecated static { resources(...) } DSL is current
  • Assumes Ktor sets cache headers for static assets by itself
  • Serves large media through the application instead of a CDN

context