Routing

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 a get request
  • store() method for a post request
  • delete() method for a delete request
  • update() method for a patch 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.

  1. Register the middleware group

//...

override fun registerRouteMiddlewareGroups(
    groups: HashMap<String,
    List<KClass<out Middleware<HttpCall>>>>) {

    groups["secret"] = listOf(SecretMiddleware::class, SuperSecretMiddleware::class)
}

//...

  1. 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.