Skip to content
Kona

Controllers

A controller is a class that groups related HTTP actions. Instead of putting every route closure in one file, you point routes at controller methods. Controllers keep validation, authorization, and response shaping next to each other.

How you register those methods on the router (including WebRoutes, groups, and resource()) is covered in Routing. This page focuses on what lives inside the controller class and the public types that support it: Controller, FormRequest, InvokableController, ResourceController, and BaseResourceController.

On each matched request, the HTTP kernel runs global middleware, resolves the route, binds path parameters onto the Request, runs route middleware, then calls the route’s Handler. A controller method is that handler when you bind it from a route table:

Request → global middleware → route match → route middleware → controller method → Response

A controller method always takes a Request and returns a Response. There is no separate “action signature” API, no automatic constructor injection into controllers, and no attribute/annotation routing — you construct the controller yourself and bind methods explicitly.

Recommended (IDE-friendly): instantiate the controller in WebRoutes.register (or ApiRoutes), then bind a lambda or method reference. The IDE can autocomplete method names, jump to the action, and flag typos at compile time.

class WebRoutes(private val app: Application) {
fun register(router: Router) {
val projects = ProjectController(app)
router.get("/projects") { projects.index(it) }.name("projects.index")
router.post("/projects") { projects.store(it) }.name("projects.store")
router.get("/projects/{id}") { projects.show(it) }.name("projects.show")
}
}

Inline handler lambdas work the same way when you do not need a controller class. Details and more verbs live on Routing.

Controllers are ordinary classes. Pass what they need in the constructor — Application, a repository, a Cadence model helper, config values from boot:

class ProjectController(private val app: Application) {
private val db = app.resolve(DatabaseManager::class.java)
fun index(request: Request): Response =
Response.json(db.table("projects").get())
}

Resolve heavy dependencies once when you construct the controller in the route table (or once in a provider), not on every request inside the action, unless the dependency is request-scoped.

./grind make:controller Name writes app/http/controllers/<Name>Controller (Kotlin or Java via --lang).

Flag Effect
(none) Single index method returning a text stub
--resource CRUD stubs: index, create, store, show, edit, update, destroy — ready for router.resource / apiResource
--lang=kotlin|java Language (defaults from the app layout)

Generated resource stubs read the path id with request.route("id"). They do not extend Controller or BaseResourceController — add those bases yourself when you want validate helpers or default 404s for unimplemented actions.

You do not have to extend Controller. Any class with Request → Response methods works as a route target.

Extend Controller when you want the protected validate helpers:

Method Behavior
validate(request, rules) Delegates to request.validate(rules) — returns validated map or throws ValidationException
validate(request, formRequest) Runs formRequest.validate(request) — authorize first, then rules
class NotesController : Controller() {
fun store(request: Request): Response {
val data = validate(request, mapOf("title" to listOf("required", "min:3")))
return Response.json(data, 201)
}
}
router.post("/notes") { notes.store(it) }

validate throws ValidationException (HTTP 422 / redirect-back) when rules fail — the same path as request.validate(...). Full rule list: Validation.

After a successful request.validate(...) (or the controller helper that calls it), request.safe() returns the same validated map. Calling safe() before a successful validate throws IllegalStateException.

Path parameters are on the request after routing. Prefer request.route("id"):

fun show(request: Request): Response {
val id = request.route("id")!!
val project = projects.find(id) ?: throw NotFoundException("Project not found.")
return Response.json(project)
}

For body and query input, use request.data(), request.input(...), or validate first — see Request & response.

A form request is a small object that owns validation rules (and optional authorization) for one action. Extend FormRequest:

Member Role
rules() Abstract — map of field → rule strings
authorize(request) Optional; defaults to true. Return false to reject
validate(request) If unauthorized → HttpException(403, "This action is unauthorized."); else Validator.make(request, rules()).validate()
class StoreNoteRequest : FormRequest() {
override fun rules(): Map<String, List<String>> =
mapOf("title" to listOf("required", "min:3"))
override fun authorize(request: Request): Boolean =
request.header("X-Token") == "secret"
}
class NotesController : Controller() {
fun store(request: Request): Response {
val data = validate(request, StoreNoteRequest())
return Response.json(data, 201)
}
}

You can also call StoreNoteRequest().validate(request) without extending Controller. Authorization failures are 403, not 422. Validation failures still become ValidationException.

There is no automatic form-request resolution from route signatures — you construct and run the form request yourself inside the action.

When a route has a single action, implement InvokableController (invoke(Request): Response) and register it as a handler:

val ping = InvokableController { Response.text("pong") }
router.get("/ping", ping.asHandler()).name("ping")
class HealthCheck : InvokableController {
override fun invoke(request: Request): Response =
Response.json(mapOf("status" to "ok"))
}
router.get("/health", HealthCheck().asHandler()).name("health")

Controllers.asHandler / Kotlin asHandler() adapts the invokable to a Handler for the router.

Router.resource(name, controller) wires the full REST map (including HTML create / edit) onto a ResourceController. Prefer apiResource for JSON APIs (no form routes).

ResourceController is an interface with seven methods: index, create, store, show, edit, update, destroy. Extend BaseResourceController when you only need some actions — unimplemented ones throw NotFoundException (HTTP 404).

class NotesController : BaseResourceController() {
override fun index(request: Request): Response = Response.json(notes.all())
override fun show(request: Request): Response {
val note = notes.find(request.route("id")!!)
?: throw NotFoundException()
return Response.json(note)
}
override fun store(request: Request): Response {
val data = validate(request, StoreNoteRequest())
return Response.json(notes.create(data), 201)
}
override fun destroy(request: Request): Response {
notes.delete(request.route("id")!!)
return Response.noContent()
}
}
router.apiResource("notes", NotesController())
router.apiResource("notes", NotesController(), only = listOf("index", "show"))

BaseResourceController extends Controller, so validate is available. Pair JSON payloads with API Resources. Filtering with only / except and the /resource/create caveat for apiResource are documented under Routing.

Controllers return the same factories as any handler — Response.json, html, text, redirect, back, noContent, download, and so on. See Request & response.

Common patterns:

fun store(request: Request): Response {
val data = validate(request, StoreNoteRequest())
val note = notes.create(data)
return Response.redirect(route("notes.show", mapOf("id" to note.id)))
.with("status", "Created")
}
fun destroy(request: Request): Response {
notes.delete(request.route("id")!!)
return Response.noContent()
}

Throw NotFoundException, HttpException, or ValidationException instead of building error envelopes by hand — see Exceptions.

class UserApiController : Controller() {
fun store(request: Request): Response {
val data = validate(request, mapOf(
"email" to listOf("required", "email"),
"name" to listOf("required", "string", "max:100"),
))
return Response.json(users.create(data), 201)
}
}

HTML form update with redirect-back on failure

Section titled “HTML form update with redirect-back on failure”

Validation failures for non-JSON clients redirect back with flashed errors (default exception handler). On success, redirect to a named route:

fun update(request: Request): Response {
val data = validate(request, UpdatePostRequest())
posts.update(request.route("id")!!, data)
return Response.redirect(route("posts.show", mapOf("id" to request.route("id")!!)))
}
class ReportDownload(private val reports: ReportService) : InvokableController {
override fun invoke(request: Request): Response =
Response.download(reports.pdf(), "report.pdf")
}
router.get("/reports/latest", ReportDownload(reports).asHandler())
.middleware(Authenticate())

Open class. Protected: validate(Request, Map), validate(Request, FormRequest).

Abstract. rules(), optional authorize(Request), validate(Request).

Functional interface: invoke(Request): Response. Adapt with asHandler() / Controllers.asHandler.

Interface: index, create, store, show, edit, update, destroy.

Abstract class extending Controller and implementing ResourceController. Default implementations throw NotFoundException().

Controllers.asHandler(InvokableController): Handler.

Do I need to extend Controller?
No. Extend it only for the validate helpers (or extend BaseResourceController for resource defaults).

Why isn’t my controller constructed from the container automatically?
Kona does not auto-resolve controllers from route class references. Instantiate them in the route table (or a factory you control).

Can I put middleware on the controller class?
Not as a class annotation. Attach middleware on the route or group — see Middleware and Routing.

Generated --resource stubs don’t implement ResourceController.
They use matching method names. For router.resource(...), either implement the interface / extend BaseResourceController, or keep binding methods one-by-one.

Where do validation errors go?
JSON clients get 422; HTML form clients get redirect-back with flashed errors — Validation.

  • Annotation / attribute-based controller routing
  • Automatic constructor injection of controllers from the router
  • Controller-level middleware attributes
  • Invokable registration without asHandler / Controllers.asHandler
  • Form-request type hints that auto-run before the action