Getting Started with Restia

TODO: Update to reflect changes in default skeleton project.

Requirements

This tutorial will assume you have

  • A working installation of any Linux distro
  • A working installation of Openresty
  • Openrestys version of nginx in the PATH as openresty
  • A working installation of Luarocks
  • The curl commandline tool

Creating a project

First of all, you will need to install Restia:

$ luarocks install restia --dev --lua-version 5.1

Some of restias dependancies may require additional libraries to be present on your system. How to install these libraryes will depend on what operating system you are using; for ubuntu-based systems you might want to use apt install.

After that, you can run restia new to create a new project:

$ restia new app
$ cd application

Now you can take a moment to inspect the directory structure. This is also a good moment to create a git repository and commit everything in the directory. There’s already a bunch of .gitignore files so you don’t commit anything unnecessary.

$ git init
$ git add --all
$ git commit -m 'Initialize project 🎉'

At this point you can already run your application:

$ restia run

And open http://localhost:8080/ in the browser to be greeted with a plain but working website.

Adding a Page

The scaffolding application generated by restia new app assumes a Model-View-Controler structure. If necessary, this can be changed, of course.

Let’s add a new page at /hi, which will say hi to the user.

Add a Controller

First, we will create the controllers/hi.lua file with the following content:

local views = require "views"

return function(req)
    return ngx.say(views.hi { name = "Restia" })
end

Restia looks for controllers in /controllers

The default location block found in locations/root sets nginx up so for any requested route foo/bar it looks for a controller in controllers/foo/bar.lua unless any other (more specific) location block matches first.

Since / is the least specific location possible, this will always be tried last and gets overridden by anything that matches the route in question.


Add a View

With the controller set up to handle the application logic, next we need a view to render some output to the user.

Templates in Restia are all just functions that accept a table of parameters and return some output as a string.

To examplify this; one could even write a template as a plain Lua file. for example, open a file views/hi.lua and add the following code:

return function(parameters) -- This function is our "template"
    return "<h1>Hello from " .. parameters.name .. "!</h1>"
end

to achieve the same result as what’s explained below. However, writing templates this way is cumbersome, which is why this would rarely be done in a real project.

Hint: If you tried this out, don’t forget to delete views/hi.lua before continuing the guide, otherwise it will override the templates you will create next ;)


Let’s start with some simple HTML. First, create a new cosmo template by opening a file views/hi.cosmo and adding the following:

<h1>Hello World!</h1>

Restia will now find this file whenever we access require("views").hi and return a function that returns the final output.

You can start restia and confirm the result if you want.

However, in most cases we'd want to add some dynamic content to our templates, otherwise we'd just be serving static files over plain nginx. Let’s do that by changing the template to:

<h1>Greetings from $name!</h1>

The $name in the template gets replaced with “Restia” because of the {name = "Restia"} we pass through in the controller.

Now, you can start restia again and look at the result in the browser. This time though, let’s just keep restia running in the background.

$ restia run &

Making the template Multistage

For small snippets, writing HTML is quite acceptable. But when you're working on a large application, all those opening and closing tags can get very cumbersome to type.

For this reason, restia has an additional templating engine called MoonXML. It’s very flexible because “templates” are really just code that runs in a special environments where missing functions are generated on the fly to print HTML tags. This also makes moonhtml templates considerably slower than a more simple templating engine like cosmo, so Restia lets you combine the two into a multistage template.

Let’s first rename the hi template to views/hi.cosmo.moonhtml and change the content to

h1 "Greetings from $name!"

Restia will now load it as a moonhtml-cosmo multistage template; that is, the first time you access the views.hi template in your code, it will load the file as a moonhtml template and render it right then and there.

However, the resulting string is not displayed yet; instead, it is loaded as a cosmo template and saved into the views table. From now on, every time you access views.hi, you will get the pre-rendered cosmo template.

What this means, is that you can insert dynamic content at both stages, which can be a bit confusing at first. To illustrate this, let’s add two timestamps to our template in views/hi.cosmo.moonhtml:

h1 "Greetings from $name!"
p "Rendered: $time"
p "Pre-rendered: "..os.date()

and modify the controller at controllers/hi.lua to pass an additional parameter:

return ngx.say(views.hi { name = "Restia", time = os.date() })

Now open the page in the browser again.

The first time you open the page, the two paragraphs should (almost) the same time. However, if you wait a few seconds and reload the page, the first timestamp should have changed, but the second one should remain the same.

Summarizing this behavior as a rule of thumb:

Cosmo parameters change on every render. They can be identified by the $ and the fact that they appear within strings.

MoonHTML expressions get evaluated only once. They appear as normal variables, function calls or other expressions in the MoonHTML template.

You can make use of this by rendering certain content in the MoonXML stage if it onle relies on information that doesn’t change once the server is running, like a navigation menu, generated URLs for resources, etc.

Also keep in mind that there are also plain, single-stage MoonHTML templates that get rehdered every time. They can be identified by the single .moonhtml file extension.

Most importantly though: The controller shouldn’t care about the type of template it’s dealing with. The controller just renders a parametrized view, which, by convention is a function as described above.

Making it bot-friendly

We already have a nice HTML page, but what about computers? They don’t like looking at HTML pages nearly as much as human users do. Instead, let’s ofer them the same content in a more machine-readable format.

To achieve this, first replace the line that renders the view with:

req:offer {
    {"text/html", function(req)
        return views.greeter { name = "Restia", time = os.date() }
    end};
}

This matches the users accept header against a list of available content-types and renders the best match. When two content-types are equally prefered by the client, it will just take the first one, so it is always a good idea to put the computationally cheaper content-types at the top.

Notice the missing ngx.say: req:offer takes care of that automatically.

Now we can add another content type to the list:

{"application/json", function(req)
    return json.encode { name = "Restia", time = os.date() }
end};

and, of course, actually define json as some module that can encode JSON data. Luckily, OpenResty comes bundled with one, so you can just add local json = require 'cjson' at the top of the file.

As you may have noticed, there’s some duplication there: the table that gets sent to the template looks identical to the one that gets encoded as JSON. This isn’t always the case, but rather often, it is. We can just extract that table into a variable and put it on top of the content negotiation code.

The final controller should look something like this:

-- Require some modules
return function(req)
    local data = { name = "Restia", time = os.date() }
    req:offer {
        {"application/json", function(req)
            return json.encode(data)
        end};
        {"text/html", function(req)
            return views.greeter(data)
        end};
    }
end

You can confirm this by calling in your terminal

curl localhost:8080/hi -H 'accept: application/json'
curl localhost:8080/hi -H 'accept: text/html'
curl localhost:8080/hi -H 'accept: application/json;q=.3,text/html;q=.5'

And in case the server has no acceptable content-type for the client, it will return an error instead:

curl localhost:8080/hi -H 'accept: application/yaml'
generated by LDoc 1.4.6 Last updated 2021-01-03 16:45:09