Cookbook
This document contains many fun recipes for cooking with mojo.js.
Concepts
Essentials every mojo.js developer should know.
Reverse Proxy
A reverse proxy architecture is a deployment technique used in many production environments, where a reverse proxy
server is put in front of your application to act as the endpoint accessible by external clients. It can provide a lot of benefits, like terminating SSL connections from the outside, limiting the number of concurrent open sockets towards the mojo.js application, balancing load across multiple instances, or supporting several applications through the same IP/port.
..........................................
: :
+--------+ : +-----------+ +---------------+ :
| |-------->| | | | :
| client | : | reverse |----->| mojo.js | :
| |<--------| proxy | | application | :
+--------+ : | |<-----| | :
: +-----------+ +---------------+ :
: :
.. system boundary (e.g. same host) ......
This setup introduces some problems, though: the application will receive requests from the reverse proxy instead of the original client; the address/hostname where your application lives internally will be different from the one visible from the outside; and if terminating SSL, the reverse proxy exposes services via HTTPS while using HTTP towards the mojo.js application.
As an example, compare a sample request from the client and what the mojo.js application receives:
client reverse proxy mojo.js app
__|__ _______________|______________ ____|____
/ \ / \ / \
1.2.3.4 --HTTPS--> api.example.com 10.20.30.39 --HTTP--> 10.20.30.40
GET /foo/1 HTTP/1.1 | GET /foo/1 HTTP/1.1
Host: api.example.com | Host: 10.20.30.40
User-Agent: Firefox | User-Agent: ShinyProxy/1.2
... | ...
However, now the client address is no longer available (which might be useful for analytics, or Geo-IP) and URLs generated via ctx.urlFor()
will look like this:
http://10.20.30.40/bar/2
instead of something meaningful for the client, like this:
https://api.example.com/bar/2
To solve these problems, you can configure your reverse proxy to send the missing data and tell your application about it with the --proxy
option.
$ node myapp.js server --proxy
Deployment
Getting mojo.js applications running on different platforms.
Built-in Web Server
mojo.js contains a very portable Node.js based HTTP and WebSocket server. It can be used for web applications of any size and scales very well.
$ node myapp.js server
Web application available at http://0.0.0.0:3000/
It is available to every application through the server
command, which has many configuration options and is known to work on every platform Node.js works on.
$ node myapp.js server -h
...List of available options...
Another huge advantage is that it supports TLS and WebSockets out of the box, a self-signed development certificate for testing purposes is built right in, so it just works.
$ node myapp.js server -l https://127.0.0.1:3000
Web application available at https://127.0.0.1:3000/
Systemd
To manage the web server with systemd, you can use a unit configuration file like this.
[Unit]
Description=My mojo.js application
After=network.target
[Service]
Type=simple
User=sri
ExecStart=NODE_ENV=production node /home/sri/myapp/myapp.js server -l http://*:8080
[Install]
WantedBy=multi-user.target
And while the default logger will already work pretty well, we also have native support for the journald format. That means if you activate the systemdFormatter
you can get proper log level mapping and syntax highlighting for your journal too.
import mojo, {Logger} from '@mojojs/core';
const app = mojo();
app.log.formatter = Logger.systemdFormatter;
app.get('/', ctx => ctx.render({text: 'Hello systemd!'}));
app.start();
You can even use systemd for socket activation. The socket will be passed to your server as file descriptor 3
, so all you have to do is to use a slightly different listen option.
ExecStart=NODE_ENV=production node /home/sri/myapp/myapp.js server -l http://*?fd=3
Reloading
After reading the Introduction you should already be familiar with nodemon. It is a restarter that starts a new web server process whenever a file in your project changes, and should therefore only be used during development.
$ npm install nodemon
...
$ npx nodemon myapp.js server
...
[39248] Web application available at http://127.0.0.1:3000/
Apache/CGI
CGI is supported out of the box and your mojo.js application will automatically detect that it is executed as a CGI script. Its use in production environments is discouraged though, because as a result of how CGI works, it is very slow and many web servers are making it exceptionally hard to configure properly. Additionally, many real-time web features, such as WebSockets, are not available.
ScriptAlias / /home/sri/myapp/index.js/
Nginx
One of the most popular setups these days is web applications behind an Nginx reverse proxy, which even supports WebSockets in newer versions.
upstream myapp {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://myapp;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Apache/mod_proxy
Another good reverse proxy is Apache with mod_proxy
, the configuration looks quite similar to the Nginx one above. And if you need WebSocket support, newer versions come with mod_proxy_wstunnel
.
<VirtualHost *:80>
ServerName localhost
<Proxy *>
Require all granted
</Proxy>
ProxyRequests Off
ProxyPreserveHost On
ProxyPass /echo ws://localhost:8080/echo
ProxyPass / http://localhost:8080/ keepalive=On
ProxyPassReverse / http://localhost:8080/
RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>
Envoy
mojo.js applications can be deployed on cloud-native environments that use Envoy, such as with this reverse proxy configuration similar to the Apache and Nginx ones above.
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 80 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: index_http
route_config:
name: local_route
virtual_hosts:
- name: service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: local_service
upgrade_configs:
- upgrade_type: websocket
http_filters:
- name: envoy.filters.http.router
typed_config:
clusters:
- name: local_service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: local_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: mojo, port_value: 8080 }
While this configuration works for simple applications, Envoy’s typical use case is for implementing proxies of applications as a “service mesh” providing advanced filtering, load balancing, and observability features, such as seen in Istio. For more examples, visit the Envoy documentation.
Application
Fun mojo.js application hacks for all occasions.
Basic Authentication
Basic authentication data will be automatically extracted from the Authorization
header.
import mojo from '@mojojs/core';
const app = mojo();
app.get('/', async ctx => {
// Check for username "Bender" and password "rocks"
if (ctx.req.userinfo === 'Bender:rocks') return ctx.render({text: 'Hello Bender!'});
// Require authentication
ctx.res.set('WWW-Authenticate', 'Basic');
return ctx.render({text: 'Authentication required!', status: 401});
});
app.start();
This can be combined with TLS for a secure authentication mechanism.
$ node myapp.js server -l 'https://*:3000?cert=./server.crt&key=./server.key'
Adding a Configuration File
Adding a configuration file to your application is as easy as adding a file to its home directory and loading the plugin jsonConfigPlugin
. The default name for the config file is config.json
, and it is possible to have mode specific config files like config.development.json
.
$ echo '{"name": "my mojo.js application"}' > config.json
Configuration files themselves are just plain JSON files containing settings that will get merged into app.config
, which is also available as ctx.config
.
import mojo, {jsonConfigPlugin} from '@mojojs/core';
const app = mojo();
app.plugin(jsonConfigPlugin);
app.get('/', async ctx => {
await ctx.render({json: {name: ctx.config.name}});
});
app.start();
Alternatively you can also use configuration files in the YAML format with yamlConfigPlugin
.
Adding Plugins to Your Application
To organize your code better and to prevent helpers from cluttering your application, you can use application specific plugins.
$ mkdir plugins
$ touch plugins/my-helpers.js
They work just like normal plugins.
export default function myHelpersPlugin (app) {
app.addHelper('renderWithHeader', async (ctx, ...args) => {
ctx.res.set('X-Mojo', 'I <3 mojo.js!');
await ctx.render(...args);
});
}
You can have as many application specific plugins as you like, the only difference to normal plugins is that you load them directly from the file.
import mojo from '@mojojs/core';
import myHelpersPlugin from './plugins/my-helpers.js';
const app = mojo();
app.plugin(myHelpersPlugin);
app.get('/', async ctx => {
await ctx.renderWithHeader({text: 'I ♥ mojo.js!'});
});
app.start();
Of course these plugins can contain more than just helpers.
Adding Commands to Your Application
By now you’ve probably used many of the built-in commands, like get
and server
, but did you know that you can just add new ones and that they will be picked up automatically by the command line interface if they are placed in a cli
directory in your application’s home directory?
$ mkdir cli
$ touch cli/spy.js
Every command is async and has full access to the application object app
.
export default async function spyCommand(app, args) {
const subCommand = args[2];
if (subCommand === 'secrets') {
console.warn(app.secrets);
} else if (subCommand === 'mode') {
console.warn(app.mode);
}
}
spyCommand.description = 'Spy on application';
spyCommand.usage = `Usage: APPLICATION spy [OPTIONS]
node index.js spy
Options:
-h, --help Show this summary of available options
`;
Command line arguments are passed right through and you can parse them with whatever module you prefer.
$ node index.js spy secrets
["s3cret"]
The options -h
and --help
are handled automatically for all commands.
Running Code Against Your Application
Ever thought about running a quick one-liner against your mojo.js application to test something? Thanks to the eval
command you can do just that, the application object itself can be accessed via app
.
$ node index.js eval 'console.log(app.static.publicPaths)'
["/home/sri/myapp/public"]
The verbose option will automatically print the return value or returned data structure to STDOUT.
$ node index.js eval -v 'app.renderer.viewPaths'
["/home/sri/myapp/views"]
Support
If you have any questions the documentation might not yet answer, don’t hesitate to ask in the Forum, on Matrix, or IRC.