View Source Controllers

Controllers are central in how Nova works. They are in charge of handling all user-implemented logic for a request. Controllers are located in the /src/controllers/ directory of your Nova application. A controller is basically a regular Erlang module that exposes functions you've provided in the routing file.

Request life-cycle

Handlers

Handlers are modules that interprets the resulting output from a controller and sends it to the requester. Handlers are identified by the first atom in the return of a controller, eg {json, #{status => "ok"}} calls the json handler. You can read about the handlers below and what their functions are.

JSON structures

Simple interface

Keyword: json

Spec: {json, Structure :: map()}

Example: {json, #{status => "ok"}}.

Converts the map (Second element in the resulting tuple) and sends it to the requester with HTTP-status code 200.

Advanced interface

Keyword: json

Spec: {json, StatusCode :: integer(), Headers :: map(), JSON :: map()}

Example: {json, 201, #{"x-correlation-id", "EX123"}, #{some_response => true}}

Same as the simple interface but with two additional elements for specifying HTTP-status code and additional headers.

HTML templates (Using Erlydtl)

Nova renders templates using ErlyDTL (https://github.com/erlydtl/erlydtl which uses the django template language to express logic. To get a better overview for the functionality that comes with dtl check their documentation page

Simple interface

Keyword: view

Spec: {view, Variables :: map() | [{Key :: atom() | binary() | string(), Value :: any()}]}

Example: {view, #{my_var => "123"}}

Renders the corresponding view with the variables attached. If a controller is named my_simple_controller.erl the view is named my_simple_view.dtl.

Advanced interface

Keyword: view

Spec: {view, Variables :: map() | [{Key :: atom() | binary() | string(), Value :: any()}], Options :: map()}

Example: {view, #{my_var => "123"}, #{view => my_view_mod}}

Same as the simple interface but where you can define some options. Currently the only option for this interface is view which enables the user to specify a view other than the corresponding one based on controllers name. In the example above the dtl-file my_view_mod.dtl would be rendered.

HTTP-status codes

Simple interface

Keyword: status

Spec: {status, StatusCode :: integer()}

Example: {status, 200}

Returns a HTTP-code to the requester with an empty body.

Medium interface

Keyword: status

Spec: {status, StatusCode :: integer(), ExtraHeaders :: map()}

Example: {status, 200, #{"content-type" => "application/json"}}

Same as the simple interface but with an additional field for specifying additional headers.

Advanced interface

Keyword: status

Spec: {status, Status :: integer(), ExtraHeaders :: map(), Body :: binary()}

Example: {status, 200, #{"content-type" => "text/plain"}, "A plain text"}

Same as the medium interface but with an additional field for specifying a body.

File transfers (Using cowboys sendfile functionality)

Keyword: sendfile

Spec: {sendfile, StatusCode :: integer(), Headers :: map(), {Offset :: integer(), Length :: integer(), Path :: list()}, Mime :: binary()}

Example: {sendfile, 200, #{}, {0, 12345, "path/to/logo.png"}, "image/png"}

Sends a file using sendfile. This uses cowboys sendfile functionality and more information about it can be found in the cowboy manual on sendfile

Redirecting user

Keyword: redirect

Spec: {redirect, Route :: list() | binary()}

Example: {redirect, "/my/other/path}

Sends a temporary redirect (HTTP status code 302) for the specified path to requester.

Fallback controllers

Phoenix have a really useful feature which they call action_fallback. We thought it would be a good addition to Nova to include something similar and therefore the fallback_controller was introduced. If a controller returns an invalid/unhandled result the fallback controller gets invoked and can take action on the payload. It's good for separating error-handling from the controllers. A fallback controller is set by setting the fallback_controller attribute with the module name of the fallback controller.

The following example shows how a controller defines a fallback

-module(my_main_controller).
-export([
    error_example/1
]).
-fallback_controller(my_fallback_controller).

error_example(_Req) ->
    %% Since {error, ...} is not a valid handler the fallback-controller will be invoked
    {error, example_error}.

A fallback controller exposes one function resolve/2 which returns a handler (like for regular controllers) in order to return the response to client. If we take the previous example and try and build a fallback controller for it:

-module(my_fallback_controller).
-export([
    resolve/2
]).

resolve(Req, {error, example_error}) ->
  {status, 400}.

Plugins

Plugins are part of the Nova core and can be executed both before and after the execution of a controller. They can both terminate a request early (like the request plugin does) or transform data into another structure (json plugin). Plugins can be defined in two different places; Global plugins are defined in the sys.config file and will be executed for every incoming request. If a plugin should only be executed for a limited set of endpoints it can be defined in the router file for that specific application (we call there local plugins).

Global plugins

Global plugins are defined in the sys.config file and can have two different states: pre and post controller. They are exectued before or after a controller. Global plugins lives under the nova application, plugins key. An example of a sys.config file:

{nova, [
  {plugins, [
    {pre_request, [
      {nova_request_plugin, #{decode_json_body => true}}
    ]},
    {post_request, [
    ]}
  ]}
]}.

In order to find the valid options one can call the plugin_info/0 function of each plugin. Currently there are four plugins shipped with Nova.

NOTE

If you are creating an application that can be included in another Nova application all of your plugins should be defined in the router-file (Local plugins) in order to avoid being overwritten.

Local plugins

Local plugins works almost as the global ones but for a limited set of paths. Local plugins are therefore declared in the router-file for each group of endpoints. They are declared in the same way as the global ones.

Example:

routes(_Env) ->
  [#{
    prefix => "/api/json",
    security => false,
    plugins => [
      {pre_request, [
        {nova_request_plugin, #{decode_json_body => true}}
      ]}
    ],
    routes => [
      {"/my_json_route", {my_app_json_controller, json}, #{methods => [get, post]}}
    ]
  }].

It's recommended to use this method of specifying routes if you plan to use it as a component in another Nova application.

Websockets

Coming soon

Callbacks

Coming soon