Controllers & Responses

Every Nova controller function receives a request map and returns a tuple that tells Nova how to respond. This chapter covers all the response types and patterns you'll use.

The request map

When Nova calls your controller function, it passes a map containing everything about the request:

show(#{method := <<"GET">>,
       bindings := #{<<"id">> := Id},
       auth_data := #{username := User}} = Req) ->
    ...

Key fields available in the request map:

KeyDescription
methodHTTP method as uppercase binary (<<"GET">>, <<"POST">>, etc.)
bindingsPath parameters (e.g. #{<<"id">> => <<"42">>})
auth_dataData from the security function (if any)
jsonDecoded JSON body (if nova_request_plugin is configured)
paramsDecoded form params (if nova_request_plugin is configured)
parsed_qsParsed query string (if nova_request_plugin is configured)
csrf_tokenCSRF token (if nova_csrf_plugin is enabled)
correlation_idRequest correlation ID (if nova_correlation_plugin is enabled)

You can also access raw Cowboy request data using cowboy_req functions on the request map.

Response tuples

JSON responses

%% 200 OK with JSON body (201 for POST requests)
{json, #{message => <<"hello">>}}

%% Custom status, headers, and body
{json, 201, #{<<"location">> => <<"/api/posts/1">>}, #{id => 1, title => <<"New Post">>}}

Template rendering

%% Render the default template (derived from module name)
{ok, [{title, <<"My Blog">>}]}

%% Render a specific template
{ok, [{error, <<"Invalid">>}], #{view => login}}

%% Render with custom status
{ok, [{error, <<"Not Found">>}], #{view => error_page, status_code => 404}}

Status codes

%% Bare status code
{status, 204}

%% Status with headers and body
{status, 404, #{}, #{error => <<"not found">>}}

Redirects

%% 302 redirect
{redirect, "/login"}

%% Redirect with modified request (e.g. after deleting a session cookie)
{redirect, "/login", Req1}

File responses

{sendfile, 200, #{}, {0, FileSize, "/path/to/file.pdf"}, <<"application/pdf">>}

Complete reference

ReturnDescription
{ok, Variables}Render the default template with variables
{ok, Variables, #{view => Name}}Render a specific template
{ok, Variables, #{view => Name, status_code => Code}}Render template with custom status
{json, Data}JSON response (status 200, or 201 for POST)
{json, StatusCode, Headers, Body}JSON response with custom status and headers
{status, StatusCode}Bare status code response
{status, StatusCode, Headers, Body}Status with headers and body
{redirect, Path}HTTP 302 redirect
{redirect, Path, Req}Redirect with modified request
{sendfile, StatusCode, Headers, {Offset, Length, Path}, MimeType}Send a file

Handling multiple HTTP methods

A single controller function can handle different methods using pattern matching:

login(#{method := <<"GET">>}) ->
    {ok, [], #{view => login}};
login(#{method := <<"POST">>, params := Params} = Req) ->
    %% process login form
    {redirect, "/"}.

Or use separate functions for clarity:

%% In the router
{"/login", fun blog_main_controller:login/1, #{methods => [get]}},
{"/login", fun blog_main_controller:login_post/1, #{methods => [post]}}

Custom response handlers

Nova uses a handler registry that maps return tuple atoms to handler functions:

nova_handlers:register_handler(xml, fun my_xml_handler:handle/3).

Then return from controllers:

my_action(_Req) ->
    {xml, <<"<user><name>Alice</name></user>">>}.

The handler function receives (ReturnTuple, CallbackFun, Req) and must return {ok, Req2}.

Fallback controllers

If a controller returns an unrecognized value, Nova can delegate to a fallback controller:

-module(blog_posts_controller).
-fallback_controller(blog_error_controller).

index(_Req) ->
    case do_something() of
        {ok, Data} -> {json, Data};
        unexpected_value -> unexpected_value  %% Goes to fallback
    end.

The fallback module needs resolve/2:

resolve(Req, InvalidReturn) ->
    logger:warning("Unexpected controller return: ~p", [InvalidReturn]),
    {status, 500, #{}, #{error => <<"internal server error">>}}.

Next, let's look at plugins and middleware — the layer that processes requests before and after your controllers.