View Source Routing

Each nova application have their own routes file. This file contains information about all the routes existing for this application.

Basic components

A simple route file could look something like this:

-module(my_app_router).

-export([routes/1]).

routes(_Environment) ->
  [#{prefix => "/admin",
    security => false,
    routes => [
      {"/", fun my_controller:main/1, #{methods => [get]}}
    ]
  }].

This will create a path for /admin which, when a user enters will call my_controller:main/1. The _Environment variable that is consumed by the routes function will have the value that nova:get_environment/0 returns. The environment variable is an important thing cause it enables the devlopers to define different routes for different environments. To change the running environment edit the sys.config file to include an {environment, Env} tuple under the nova application where Env can be any erlang-term.

The routing object

The routing object consists of three or four fields.

HTTP(S) Routing

{Route :: list(), ControllerCallback :: function(), Options :: map()}

As you saw in the initial example in the Basic components section, we defined a route for the root path /.

Websocket routing

{Route :: list(), Controller :: atom(), Options :: map()}

Important

One needs to define protocol => ws in the options-map in order to enable websocket communications.

Static files

One can also define static files to be served by nova. This is done by adding a tuple to the route entries. The value of this tuple should be of size 2 or 3 where the first element is the url and the second element is the path to the file on the filesystem related from the apps priv directory. An additional third element can be added to the tuple to define options for this particular static file or directory.

Note! The notation [...] can be used as a wildcard (Zero or many occurences) in the url-section. Valid options is;

  • mimetype - which mimetype the file should be served as. If not defined nova will try to guess the mimetype based on the file extension.
  • index_files - a list of filenames that can be used as an index. This is relevant if a directory is served.
  • list_dir - Set to true if allowing the requester to list the content of a directory (if such is served)

Example:

{"/my/static/directory/[...]", "assets/a-local-dir", #{list_dir => true}},
{"/with-index/[...]", "assets/another-dir", #{index_files => ["index.html"]}}

How to create routes

A route consists of three different components:

  • Path - This is the actual path to the endpoint. Eg "/admin"
  • Method - What method you want to support for this endpoint (get, post, update, delete). If you want to support all methods you can use the '_'-atom.
  • ControllerCallback - What erlang callback should be called on when the path gets called. This is defined as a function reference, eg fun my_controller:main/1.

Using prefix

You can group paths where you prefix them with a path. This is especially useful when having several different nova applications running. A very common example would be if you had an administration interface. Then the prefix would be "/admin". Another example is versioned APIs.

Prefix is defined at top level of a route-entry which can be seen in the Basic components section of this chapter.

Secure routing

When building web applications, ensuring the security of your routing layer is critical. Nova Framework’s routing system is designed with flexibility and security in mind, but developers must implement best practices to protect their applications from common vulnerabilities. This section outlines key considerations and features of Nova's routing system to help you achieve secure routing.

Invoking security functions for a set of endpoints

You can define a security function that will be called before the actual controller is called. This is useful if you want to check if the user is allowed to access the endpoint. The security function should return a boolean value (Or a special one - more about that later in this section). If the function returns false the request will be stopped and a 401 status code will be returned.

The following code will invoke security_controller:do_security/1 before calling the actual controller to do the security check.

-module(my_example_app_router).
-behaviour(nova_router).

-export([routes/1]).

routes(_Environment) ->
    [#{prefix => "/admin",
        security => fun security_controller:do_security/1,
        routes => [
            {"/", fun my_controller:main/1, #{methods => [get]}}
        ]
    }].

The security-function callback

The most simple way to implement a security function is to return a boolean value. If the function returns false the request will be stopped and a 401 status code will be returned. There's also an additional return value that can be used to store data that will be available to the controller. This is particularly useful if you want to pass data like user information, roles, etc. to the controller.

Return values for the secure function

Return valueDescription
trueThe request will continue to the controller.
{true, Data :: term()}The request will continue to the controller and the data will be available in the Req object under the auth_data key.
{redirect, Url :: binary()}The request will be redirected to the specified URL with HTTP status 302.
falseThe request will be stopped and a 401 status code will be returned.
{false, Headers :: map()}Same as above but also adds additional headers to the response.
{false, StatusCode :: integer(), Headers :: map()}Same as above but uses a custom status code.
{false, StatusCode :: integer(), Headers :: map(), Body :: iodata()}Same as above but also adds a custom body to the response.

Note If false is returned in any form the request will be stopped from executing the controller.

-module(security_controller).
-export([do_security/1]).

do_security(Req) ->
    case get_user(Req) of
        {ok, User} ->
            {true, #{user => User}};
        _ ->
            false
    end.

get_user(Req) ->
    ## Do some validatation and return the user
    {ok, #{name => "John Doe", role => "Admin"}}.

Once the security function has been called, the data will be available in the Req object under the auth_data key.

-module(my_controller).
-export([main/1]).

main(Req = #{auth_data := User}) ->
    io:format("User: ~p~n", [User]),
    {ok, Req, <<"Hello world!">>}.

Using plugins local to a set of endpoints

It's possible to configure a small set of endpoints with a specific plugin. This is done by adding a plugins key to the route entry. The value of this key should be a list of plugins to use for this route entry.

#{prefix => "/admin",
  plugins => [
    {pre_request, nova_json_schemas, #{render_errors => true}}
  ],
  routes => [
    {"/", fun my_controller:main/1, #{methods => [get]}}
  ]
}

In the example above we have enabled the pre-request-plugin nova_json_schemas for all routes under the /admin prefix. This will cause all requests to be validated against the JSON schema defined in the nova_json_schemas plugin. You can also include post-request-plugins in the same way.

Adding routes programatically

You can also add routes programatically by calling nova_router:add_route/2. This is useful if you want to add routes dynamically. The spec for it is:

%% nova_router:add_route/2 specification
-spec add_route(App :: atom(), Routes :: map() | [map()]) -> ok.

First argument is the application you want to add the route to. The second argument is the route or a list of routes you want to add - it uses the same structure as in the regular routers.

nova_router:add_route(my_app, #{prefix => "/admin", routes => [{"/", fun my_controller:main/1, #{methods => [get]}}]}).

This will add the routes defined in the second argument to the my_app application.

Note: If a route already exists it will be overwritten.