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'