Routing
- Getting Started
- Defining Routes
- Route Parameters
- Wildcard Route
- Route Attributes
- Route Groups
- Named Middleware Group
- Guarded Routes
- Form Method Spoofing
- Route Helpers
Routing in Alpas is strongly typed, making it very easy to navigate and refactor. It also makes some good educated guesses based on conventions for your method names and paths. This means you can succinctly write your routes without compromising the expressiveness and clarity — something that Alpas is really well-known for.
Getting Started
Start by registering your app routes on an instance of dev.alpas.routing.Router
. Although you can do this
from anywhere, Alpas convention is to add them in the routes.kt
file and then load them in the
providers/RouteServiceProvider.kt
class. When scaffolding a project,
both of these files are created for you and are already wired to load your routes. All you
need to do is add your routes in the routes.kt
file.
Alpas routing supports all the routes that respond to any HTTP verbs: get
, post
, put
, patch
, delete
, and
options
.
Defining Routes
Lambda Routes
At the very minimum, you can register a route where the first parameter is a path and the second
parameter is a function literal
with HttpCall
as the receiver.
fun Router.addRoutes() {
get("/") {
reply("Hello, World!")
}
// The root path "/" is optional. So you can shorten it to this.
get {
reply("Hello, World!")
}
post("ping") {
reply("pong")
}
}
Controller Routes
If your code for responding to an HTTP call is more complex, you can pass in the function name of a controller method as the second parameter. This is the recommended way of setting up your routes in Alpas.
fun Router.addRoutes() {
// When the request matches /docs route, the index
// method of DocsController class is invoked.
get("docs", DocsController::index)
}
While using controller routes, the function name is actually optional. Alpas follows some conventions to determine what controller action to call when a path matches:
index()
method for aget
requeststore()
method for apost
requestdelete()
method for adelete
requestupdate()
method for apatch
request
fun Router.addRoutes() {
get<DocsController>("docs") // calls DocsController#index()
post<DocsController>("docs") // calls DocsController#store()
delete<DocsController>("docs") // calls DocsController#delete()
update<DocsController>("docs") // calls DocsController#update()
}
Route Parameters
Required Parameters
If you want to capture parameters within your route, you can do so by wrapping a parameter name with angle brackets
<>
. You can later access these captured values from your controller.
fun Router.addRoutes() {
// page is a required parameter
get<DocsController>("/docs/<page>")
// both post_id and comment_id are required parameters
get<PageController>("/posts/<post_id>/comments/<comment_id>")
}
Wildcard Route
With a wildcard you can match and capture any route components. It is a catch-all route. These kinds of routes are very helpful if you want any paths to match to the same handler. A wildcard route is very common and comes in very handy if you are writing a single page application (SPA). Alpas supports a wildcard routing by adding two routes:
fun Router.addRoutes() {
// These matches all of /docs, /docs/index, /docs/page/toc, /docs/index?page=1, etc.
get("docs/<pathParamName:path>", DocsController::handleEverything)
get("docs", DocsController::handleEverything)
}
In the above example, all your paths params will be captured by pathParamName
variable. So, for a path like
docs/page/toc
, the value of pathParamName
will be page/toc
. Query parameters are separately captured
and their values are available under their respective keys.
Route Attributes
Route Name
You can set names for your routes to make it easy to refer them from your code, especially while generating URLs.
Setting a name for a route gives you the flexibility of changing its path without having to refactor
everywhere it is referenced. To give a name to a route, just call name()
method on the route.
fun Router.addRoutes() {
get<DocsController>("docs").name("docs.show")
}
Once a name is set, you can call this route from anywhere by its name instead of its actual path.
fun index(call: HttpCall) {
// get the url
val url = route("docs.show")
// redirect the call to a named route
call.redirect().toRouteNamed("docs.show")
}
// in templates
<a name="{{ route('docs.show') }}">Docs</a>
Route Middleware
If you want to apply a middleware to a route, you can pass the class name of the middleware
by calling the middleware()
method on the route. If you want, you can also pass a list of middleware classes.
fun Router.addRoutes() {
get<DocsController>("docs").middleware(MyMiddleware::class)
get<DocsController>("admin").middleware(MyMiddleware::class, AnotherMiddleware::class)
}
Route Groups
Instead of repeating common attributes, such as path prefix, name, middleware, etc., for each route, you can instead use route groups. Grouping routes helps you better organize your routes making them more readable and maintainable.
Group Path Prefix
You can set a prefix for a group. The prefix gets prepended to each route as if it was a path.
fun Router.addRoutes() {
group("docs") {
// matches /docs path
get(DocsController::index)
// matches /docs/toc path
get("toc", DocsController::showToc)
// matches /docs/latest path
get("latest", DocsController::showLatest)
}
}
Nested Route Groups
Not just a route, but you can also nest groups within another route group. Each route within a group and sub-groups receives all the merged attributes from its parents as well as its grand parents.
fun Router.addRoutes() {
group("docs") {
group("latest") {
// Receives all the attributes from both "docs" and "latest" groups.
// matches /docs/latest path
get<DocsController>()
}
}
}
Group Name
You can give a name to a route group, which will then get prepended to each route's name within the group with a dot (.) in between the names.
fun Router.addRoutes() {
group("/docs") {
// will be available as "docs.show"
get<DocsController>().name("show")
}.name("docs")
group("/admin") {
group("/profile") {
// will be available as "admin.profile.show"
get<ProfileController>().name("show")
}.name("profile")
}.name("admin")
}
Group Middleware
Just like assigning middleware to a route, you can also assign middleware to a group. The middleware gets applied to all its grandchildren routes.
/alert/ The order of middleware really matters when passing an
HttpCall
through all the assigned middleware of a route. They are applied in inside-out and left-to-right order.
fun Router.addRoutes() {
group("admin") {
group("profile") {
get<ProfileController>()
// The middleware assigned to this route, in order, are:
// 1. SecretMiddleware 2. SuperSecretMiddleware
// 3. ProfileMiddleware 4. AdminMiddleware
// 5. AuthOnlyMiddleware
get<ProfileController>("secret")
.middleware(SecretMiddleware::class, SuperSecretMiddleware::class)
}.middleware(ProfileMiddleware::class)
}.middleware(AdminMiddleware::class, AuthOnlyMiddleware::class)
}
Named Middleware Group
Instead of assigning a list of middleware to a group or to a route, sometimes it is more convenient to make a list of middleware, give it a name, and then assign this name. This allows you to share a middleware group and uniformly apply it to different routes and route groups.
To create a named middleware route, you need to first register your middleware group inside HttpKernel
's
registerRouteMiddlewareGroups()
method with a name and then call middlewareGroup()
on your routes,
or your route groups, by passing the name.
- Register the middleware group
//...
override fun registerRouteMiddlewareGroups(
groups: HashMap<String,
List<KClass<out Middleware<HttpCall>>>>) {
groups["secret"] = listOf(SecretMiddleware::class, SuperSecretMiddleware::class)
}
//...
- Apply the middleware group name
fun Router.addRoutes() {
group("admin/profile") {
// all the middleware defined under key 'secret'
// will be applied to this route
get<AdminProfileController>("secret")
}.middlewareGroup("secret")
group("user/profile") {
// all the middleware defined under key 'secret'
// will be applied to this route as well
get<UserProfileController>("secret")
}.middlewareGroup("secret")
}
Guarded Routes
Authorized Users Only
If you want a route to be accessible only to authorized users, you can either apply
AuthOnlyMiddleware
middleware or call mustBeAuthenticated()
method. Just like other route attributes,
the mustBeAuthenticated()
method can be called either on a route or on a route group.
Guests Only
Similarly, if you want a route to be accessible only if a user is not authenticated, such as a login
route, you can either apply GuestOnlyMiddleware
middleware or call mustBeGuest()
method.
Verified Users Only
If you want a route to be accessible only if a user is verified, you can either
apply VerifiedEmailOnlyMiddleware
middleware or call mustBeVerified()
method on a route or a route group.
fun Router.addRoutes() {
// anyone can access this route
get<WelcomeController>()
// only guests can access this route
get("docs", DocsController::index).mustBeGuest()
// only logged in users can access this route
get("profile", UserController::index).mustBeAuthenticated()
// only authenticated users who have also verified
// their email addresses can access this route
get("admin", AdminController::index).mustBeAuthenticated().mustBeVerified()
}
Form Method Spoofing
HTTP forms only support GET or POST but not PUT, PATCH, or DELETE. To use these methods in your form
so that the correct route gets matched, you need to spoof it by passing a hidden field named _method
with your form.
For your convenience, Alpas also comes with a {{ spoof() }}
view function that you can use to create the hidden field
for you. spoof()
takes the name of the method you want to spoof — one of PUT, PATCH, or, DELETE methods.
<form action="/docs" method="post">
{{ spoof('delete') }}
{# <input type="hidden" name="_method" value="delete"/> #}
<button type="submit">Delete</button>
</form>
Method spoofing is enabled by default. But you can disable it by setting allowMethodSpoofing
property to false
in your AppConfig
class.
Route Helpers
Template Helpers
route(name, params)
Creates a full URL for the route of the given name name
. params
is a map of parameters
expressed as if like a JSON object: {'page': 'routing', 'ver': '2'}
All required path parameters for the route are first extracted and set and then any remaining values from the map are passed as query parameters to the route.
<!-- the url will be something like: https://example.com/docs/routing?ver=2 -->
<a href="{{ route('docs.show', {'page': 'routing', 'ver': '2'}) }}">
Show Routing Docs
</a>
hasRoute(name)
Checks if a route of name
exists.
routeIs(name)
Checks if the name of the current request route matches name
.
routeIsOneOf(names)
Checks if the name of the current route matches any of the names
.
{% if routeIsOneOf(['docs.index', 'docs.toc']) %}
<h1>Hello Index and TOC!</h1>
{% endif %}
You can negate the check like so:
{% if not routeIsOneOf(['docs.index', 'docs.toc']) %}
<h1>Hello, page!</h1>
{% endif %}
Controller Helpers
route(name: String, params: Map<String, Any>? = null, absolute: Boolean = true)
Creates a full URL for a route of the name
. Set absolute
to false
if you want to get a URL
relative to server's address i.e. /docs/toc
instead of https://alpas.dev/docs/toc
.
Alpas Route Commands
alpas route:list
Lists all your app's routes with some route attributes such as method name, path, route name, actual handler, auth channel type etc.