Version: v0.9.0

Routing / URLs

The job of a Router in a web application is to route all the HTTP requests to the code that handles them, that is to say, the controllers.

Kalgan's routing system depends on the crate kalgan_router, which allows to handle complex sets of routes. It's completely decoupled from the framework, which means it and can be used outside of Kalgan as an independent component. However the opposite is not true, Kalgan depends heavily on this crate and it's not expected to work with a different router.

Kalgan's routing system is very different to the one provided by most of the Rust Frameworks currently available. Indeed, it's inspired by two frameworks which doesn't have anything to do with Rust's ecosystem: Django (Python) and Symfony (PHP).

Instead of having the routes hardcoded in every controller as it is usually seen, they are decopled from Rust source code and kept in .yaml files. These way forward presents several advantages:

  • Separation of Concerns:

    Routes should never be hardcoded with handlers. They are meant to do different things and they should be encapsulated in different components. This philosophy allows to have a general view and manage easily all the routes, the validation requirements and the controllers/middlewares which handle the request. Instead of having every route scattered with the controllers, we just group all of them in a single file/folder.

  • Named URLs:

    Every route has a name, which is really useful when performing URL reversing. However, thanks to this system not only the URI is decoupled from the route but from the handlers as well.

  • Fast Developing:

    While developing ( environment.is_prod: false ) we can amend the routes and just reload the browser to get the results. We don't need to restart the app, which will save us a huge amount of time.

Creating routes

As we have already said, in Kalgan routes are defined in .yaml files. We can locate these files wherever we want. The only important thing is to tell Kalgan the path of this file or folder. We must set this path in our settings.yaml. By convention it's recommended to locate our routing files inside the folder config/route :

router:
    path: config/route

Every route file must start with the parameter routes: which includes the array of routes. Every route definition must start by its name. By convention, it's expected to follow a snake_case naming syntax. Finally, we'll set the Route data as a block inside.

For example:

routes:
...
    - blog_article_read:
        path: /{lang}/blog/article/{date}/{title}
        controller: blog/article_controller::read
        middleware: blog_middleware::check
        methods: get
        language: "{lang}"
        requirements:
            date: "^\\d{4}-\\d{2}-\\d{2}"
            title: "[^/]+"
...

Find a description of these fields in the table below:

routes.{name} Name of the route. It helps to put the Route in context and it's necessary when generating the URI. It contains the rest of the fields.
routes.{name}.path Uri of the Route. This field is compulsory..
routes.{name}.controller Path of the controller that will handle the request. By convention / and :: are used as folder and module separators respectively. This field is compulsory. See Middlewares in the docs.
routes.{name}.middleware Path of middleware that will handle the request before the controller. By convention / and :: are used as folder and module separators respectively. See Middleware Outcome in the docs.
routes.{name}.methods List of HTTP methods supported in the request. These methods must be sepparated by a coma. For example: methods: get, post. If this field is not present any method is supported.
routes.{name}.language Language supported in the request. See i18n / Translation in the docs.
routes.{name}.requirements.{parameter} Collection of requirements for each parameter defined in the route.

Route parameters

In a real world application some of our routes aren't static, which means they are going to have an unknow and avariable value. At the same time, it's important to keep them readable and with a semantic meaning. Let's stop here to recommend to take a look at the article Cool URIs don't change written by Tim Berners-Lee, father of the World Wide Web.

In the example above we had given to the field path the value /{lang}/blog/article/{date}/{title}, which has some words within curly braces { }. These are called a parameters, which means they have a variable value. These parameters can be handle latter by the controller and/or the middleware defined in the route.

Route parameters must follow some rules or requirements, which can be defined in the field routes.{name}.requirements.{parameter}. Basically, these requirements are regular expressions based on the crate regex.

"[^/]+" All characters are allowed except slash (/). This is the default requirement for every parameter.
".+" All characters are allowed. Notice this parameter should be located at the end of the uri, otherwise it will override the following ones.
"^[0-9]+" Only numbers are allowed.
"^[0-9a-zA-Z]+" Only alphanumeric characters are allowed.
"^[a-zA-Z]+" Only alphabetic characters are allowed.
"^\\d{4}-\\d{2}-\\d{2}" Date with the format yyyy-mm-dd.

Route matching strategy

Kalgan delegates this task to crate kalgan_router, which is executed through the method Router.get_route().

The first step the Router takes is to check the HTTP method. If this is not included in the list (field methods) the router jumps to the next route. If this field is not present any method is valid.

We must be aware that the router system starts to parse the routes from to the top of the file to the bottom. When it finds a match, it stops searching for new ones.

For example, given the uri /blog/article/collection the first of the routes below will be match:

routes:
    - blog_article_read:
        path: /blog/article/{title}
        controller: blog/article_controller::read
    - blog_article_index:
        path: /blog/article/collection
        controller: blog/article_controller::index

Which at first it would be unexpected. Remember to locate the routes in the appropiate order.

When working with multiple routing files this issue might become a serious headache. It might not be so easy to find the collision as we don't know which file is going to be parsed first. In such cases we must be careful and follow an appropriate strategy in file splitting. For example, be sure that every route in the same file starts with the same slug.

For example:

routes:
    - blog_article_index:
        path: /blog/article/collection
        controller: blog/article_controller::index
    - blog_article_read:
        path: /blog/article/{title}
        controller: blog/article_controller::read
routes:
    - user_index:
        path: /user/collection
        controller: user_controller::index
    - user_read:
        path: /user/{name}
        controller: user_controller::read

Route handlers definition

In the example above we gave to the field controller the value blog/article_controller::read, which is the path to the controller. This means that function read is inside article_controller and this file inside folder blog.

We must know that by convention / and :: are used as folder and module separators respectively. However, feel free to take more rusty approach and go just with ::. Be aware that the Router converts all / to ::.

On the other hand, notice that the parent folder is not present in the path. This is because Kalgan is working with relative routes based on the functions controller::resolver and middleware::resolver which we passed as parameters in kalgan::run:

...
pub(crate) mod controller;
pub(crate) mod middleware;

fn main() {
    kalgan::run("settings.yaml", controller::resolver, middleware::resolver);
}
...

If we have followed the conventions, our controllers should be located in src/controllers/ and the middlewares in src/middlewares/.

For more informatin regarding the Controllers and Middlewares go to Handlers in the docs.

Localized Routes (i18n)

In the example above we gave to the optional field language the value {lang}}, which matches with the first parameter of our route. This means that the language adopts the value of this parameter.

For example, given the uri /en/blog/article/2022-01-22/article-title, the language code of the route would be en.

But sometimes we don't want to hard-code the language code in the path of our route. In these cases we just have to set the language code in language. For example: language: en. When this field is not present the route adopts the default language set in the settings file.

For more information regarding language support go to i18n / Translation in the docs.

The Struct Route

The kalgan_router crate provides the following Struct:

...
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Route {
    name: String,
    path: String,
    #[serde(skip_serializing, skip_deserializing)]
    path_split: Vec,
    methods: Vec,
    controller: String,
    middleware: String,
    pub parameters: HashMap,
    pub language: String
}
...

Notice that most of the fields but parameters and language are private, which means they cannot not be access directly. For this purpose we have to use the methods commonly known as getters to get their value (for example Route.get_path() ). The only field which is not accesible is path_split, which is only meant for the route collection generation.

However, this Struct is not visible through the framework and it's only accesible withing the Struct kalgan::http::request::Request. For more information regarding this Struct go to Request / Response in the docs.

Generating URIs

In Rust files

The crate kalgan_router supports the method Router.get_uri to generate a URI by passing the name of the route and the parameters, if exist.

However, this method is not visible through the framework. For this reason Kalgan offers the service url::generate which in turn calls this function. Even better, we count with the macro url! we got with the build script when we launched our app for the first time. Both functions do the same job but the with latter the list of parameters is optional.

Find below a couple of examples:

routes:
    - home:
        path: /homepage
        controller: home_controller::index
    - user:
        path: /user/{name}/{surname}
        controller: user_controller/read
...
let my_home_url: String = url!("home"); // generates: "/homepage"
let john_doe_url: String = url!("user", hashmap!("name" => "john", "surname" => "doe")); // generates: "/user/john/doe"
...

Notice that for the sake of readability we're calling the macro hashmap! in the second example.

In html files

Kalgan offers the built-in filter url for Tera template engine to render URIs in html templates. The variable is the name of the route and parameters, if present, are passed as named arguments.

For example (using the same routes than in the previous example):

...
<a href="{{ "home"|url }}">Link to My Home</a> <!-- generates: <a href="/homepage">Link to My Home</a> -->
<a href="{{ "user"|url(name="john", surname="doe") }}">Link to John Doe</a> <!-- generates: <a href="/user/john/doe">Link to John Doe</a> -->
...