URL generation
URL generation turns route names and paths into strings your app can put in JSON, redirects, emails, or HTML. Kona registers a singleton UrlGenerator when you boot with HttpServiceProvider. The same router that serves requests backs named-route lookups — name your routes first (Routing).
How it fits together
Section titled “How it fits together”HttpServiceProviderbindsRouterandUrlGenerator.- The generator is constructed with the shared
Router,app.urlas root (or""), andapp.keyas the signing key (or"kona-app-key"). Url.setGenerator(generator)installs global helpers:Url.route,Url.to/Url.url, and Kotlin top-levelroute(...)/url(...).- Controllers and views call those helpers instead of hard-coding paths.
If you call Url.route before the generator is configured, you get IllegalStateException("URL generator not configured").
Resolve the generator
Section titled “Resolve the generator”val urls = app.resolve(UrlGenerator::class.java)// Optional overrides after boot:urls.setRoot("https://example.com")urls.setKey(app.config.getString("app.key", "kona-app-key"))UrlGenerator urls = app.resolve(UrlGenerator.class);urls.setRoot("https://example.com");urls.setKey(app.config.getString("app.key", "kona-app-key"));setRoot trims a trailing /. An empty root means helpers return path-only strings (/users/42). Absolute http:// / https:// paths passed to to(...) pass through unchanged.
Named routes
Section titled “Named routes”Give routes a name, then generate URLs (with optional parameters):
router.get("/users/{id}") { users.show(it) }.name("users.show")
val pathOnly = router.route("users.show", mapOf("id" to "42"))// "/users/42" — path only, no root
val absolute = urls.route("users.show", mapOf("id" to "42"))// "https://example.com/users/42" when root is set
val viaHelper = route("users.show", mapOf("id" to "42")) // Url.routeResponse.redirect(route("users.show", mapOf("id" to id)))router.get("/users/{id}", users::show).name("users.show");
String pathOnly = router.route("users.show", Map.of("id", "42"));String absolute = urls.route("users.show", Map.of("id", "42"));String viaHelper = Url.route("users.show", Map.of("id", "42"));return Response.redirect(Url.route("users.show", Map.of("id", id)));Parameter substitution rules
Section titled “Parameter substitution rules”Substitution uses Router.route under the hood:
- Each
{key}is replaced with the matching map value. - Values are not percent-encoded — encode reserved characters yourself.
- Missing keys leave
{key}in the output. - Unknown route name →
IllegalArgumentException("Route [name] not defined"). - Duplicate names → the first registered route wins.
- Group name prefixes apply at registration time (
api.+health→api.health).
Paths without names
Section titled “Paths without names”to(path) / Url.to / url(path) join a relative path onto the configured root:
| Input | Root https://example.com |
Result |
|---|---|---|
/users |
set | https://example.com/users |
users |
set | https://example.com/users (leading / added) |
https://other.test/x |
any | https://other.test/x (unchanged) |
/users |
empty | /users |
Signed URLs
Section titled “Signed URLs”A signed URL includes an HMAC-SHA256 signature (hex) and optionally an expires unix timestamp. Use it for one-time downloads, email confirmations, or any link that must not be tampered with.
val link = urls.signed("/downloads/report", expirationSeconds = 600)// https://example.com/downloads/report?expires=…&signature=…
val named = urls.signedRoute( "users.show", mapOf("id" to "42"), expirationSeconds = 120,)
val forever = urls.signed("/invite/accept") // no expires query paramString link = urls.signed("/downloads/report", 600L);String named = urls.signedRoute("users.show", Map.of("id", "42"), 120L);String forever = urls.signed("/invite/accept"); // expirationSeconds nullHow signing works
Section titled “How signing works”- Split any existing query string from the path.
- Optionally add
expires= now +expirationSeconds. - Sort query keys, URL-encode keys and values, build
path?sortedQuery. - Sign that string with HMAC-SHA256 using the generator’s key.
- Append
signature=<hex>and prefix withto(...).
Verification rebuilds the payload from request.path plus all query params except signature, sorted the same way. If expires is present and in the past, the signature is invalid.
Verify incoming requests
Section titled “Verify incoming requests”if (!urls.hasValidSignature(request)) { throw HttpException(403, "Invalid signature.")}if (!urls.hasValidSignature(request)) { throw new HttpException(403, "Invalid signature.");}Or register ValidateSignature middleware so unsigned or expired links become HTTP 403 with message Invalid signature.:
router.get("/downloads/report") { request -> Response.download(reportBytes, "report.pdf")}.middleware(ValidateSignature(urls))router.get("/downloads/report", request -> Response.download(reportBytes, "report.pdf")) .middleware(new ValidateSignature(urls));Global Url helpers
Section titled “Global Url helpers”| Helper | Behavior |
|---|---|
Url.setGenerator / clearGenerator |
Install or clear (tests) |
Url.generator() |
Current generator or throw |
Url.route(name, params) |
Absolute/rooted named URL |
Url.to(path) / Url.url(path) |
Join path onto root |
Kotlin route(...) / url(...) |
Top-level wrappers |
Common use cases
Section titled “Common use cases”Redirect after create
Section titled “Redirect after create”return Response.redirect(route("posts.show", mapOf("id" to post.id)))JSON links
Section titled “JSON links”Response.json(mapOf( "self" to route("users.show", mapOf("id" to user.id)), "avatar" to url("/storage/avatars/${user.id}.png"),))Temporary download link
Section titled “Temporary download link”val downloadUrl = urls.signedRoute("files.show", mapOf("id" to fileId), 300)mailer.send(user.email, "Your file", downloadUrl)API reference
Section titled “API reference”UrlGenerator
Section titled “UrlGenerator”| Method | Role |
|---|---|
setRoot(root) / setKey(key) |
Configure absolute URLs and HMAC key |
to(path) |
Root + path (or pass-through absolute URL) |
route(name, params) |
Named route via router, then to |
signed(path, expirationSeconds?) |
Signed path URL |
signedRoute(name, params, expirationSeconds?) |
Named path then signed |
hasValidSignature(request) |
Verify query signature (+ optional expiry) |
ValidateSignature
Section titled “ValidateSignature”Middleware wrapping hasValidSignature; throws HttpException(403, "Invalid signature.") on failure.
Pitfalls and FAQ
Section titled “Pitfalls and FAQ”Why is my named URL missing the host?
app.url is empty. Set APP_URL / app.url, or call setRoot.
Why does substitution leave {id} in the string?
The params map did not include id. Check the key name.
Can I sign POST bodies?
No. Signatures cover path + query only.
Does changing query order break verification?
No — keys are sorted before sign and verify.
Should I use the default kona-app-key in production?
No. Set a strong app.key / APP_KEY.
What is not available today
Section titled “What is not available today”route(..., absolute = false)as a separate API (userouter.routefor path-only)- Signed middleware that allows relative expiry headers beyond the
expiresquery param - Automatic HTTPS forcing in the generator
- Asset URL helpers (Crema
@assetis separate — see Crema forms / rendering)
Next steps
Section titled “Next steps”- Register named routes in Routing.
- Protect downloads with Middleware.
- Return transformed models from API Resources.