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.

  1. ..........................................
  2. : :
  3. +--------+ : +-----------+ +---------------+ :
  4. | |-------->| | | | :
  5. | client | : | reverse |----->| mojo.js | :
  6. | |<--------| proxy | | application | :
  7. +--------+ : | |<-----| | :
  8. : +-----------+ +---------------+ :
  9. : :
  10. .. 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:

  1. client reverse proxy mojo.js app
  2. __|__ _______________|______________ ____|____
  3. / \ / \ / \
  4. 1.2.3.4 --HTTPS--> api.example.com 10.20.30.39 --HTTP--> 10.20.30.40
  5. GET /foo/1 HTTP/1.1 | GET /foo/1 HTTP/1.1
  6. Host: api.example.com | Host: 10.20.30.40
  7. User-Agent: Firefox | User-Agent: ShinyProxy/1.2
  8. ... | ...

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:

  1. http://10.20.30.40/bar/2

instead of something meaningful for the client, like this:

  1. 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.

  1. $ 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.

  1. $ node myapp.js server
  2. 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.

  1. $ node myapp.js server -h
  2. ...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.

  1. $ node myapp.js server -l https://127.0.0.1:3000
  2. 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.

  1. [Unit]
  2. Description=My mojo.js application
  3. After=network.target
  4. [Service]
  5. Type=simple
  6. User=sri
  7. ExecStart=NODE_ENV=production node /home/sri/myapp/myapp.js server -l http://*:8080
  8. [Install]
  9. 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.

  1. import mojo, {Logger} from '@mojojs/core';
  2. const app = mojo();
  3. app.log.formatter = Logger.systemdFormatter;
  4. app.get('/', ctx => ctx.render({text: 'Hello systemd!'}));
  5. 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.

  1. 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.

  1. $ npm install nodemon
  2. ...
  3. $ npx nodemon myapp.js server
  4. ...
  5. [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.

  1. 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.

  1. upstream myapp {
  2. server 127.0.0.1:8080;
  3. }
  4. server {
  5. listen 80;
  6. server_name localhost;
  7. location / {
  8. proxy_pass http://myapp;
  9. proxy_http_version 1.1;
  10. proxy_set_header Upgrade $http_upgrade;
  11. proxy_set_header Connection "upgrade";
  12. proxy_set_header Host $host;
  13. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  14. proxy_set_header X-Forwarded-Proto $scheme;
  15. }
  16. }

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.

  1. <VirtualHost *:80>
  2. ServerName localhost
  3. <Proxy *>
  4. Require all granted
  5. </Proxy>
  6. ProxyRequests Off
  7. ProxyPreserveHost On
  8. ProxyPass /echo ws://localhost:8080/echo
  9. ProxyPass / http://localhost:8080/ keepalive=On
  10. ProxyPassReverse / http://localhost:8080/
  11. RequestHeader set X-Forwarded-Proto "http"
  12. </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.

  1. static_resources:
  2. listeners:
  3. - name: listener_0
  4. address:
  5. socket_address: { address: 0.0.0.0, port_value: 80 }
  6. filter_chains:
  7. - filters:
  8. - name: envoy.filters.network.http_connection_manager
  9. typed_config:
  10. "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
  11. codec_type: auto
  12. stat_prefix: index_http
  13. route_config:
  14. name: local_route
  15. virtual_hosts:
  16. - name: service
  17. domains: ["*"]
  18. routes:
  19. - match:
  20. prefix: "/"
  21. route:
  22. cluster: local_service
  23. upgrade_configs:
  24. - upgrade_type: websocket
  25. http_filters:
  26. - name: envoy.filters.http.router
  27. typed_config:
  28. clusters:
  29. - name: local_service
  30. connect_timeout: 0.25s
  31. type: strict_dns
  32. lb_policy: round_robin
  33. load_assignment:
  34. cluster_name: local_service
  35. endpoints:
  36. - lb_endpoints:
  37. - endpoint:
  38. address:
  39. 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.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. app.get('/', async ctx => {
  4. // Check for username "Bender" and password "rocks"
  5. if (ctx.req.userinfo === 'Bender:rocks') return ctx.render({text: 'Hello Bender!'});
  6. // Require authentication
  7. ctx.res.set('WWW-Authenticate', 'Basic');
  8. return ctx.render({text: 'Authentication required!', status: 401});
  9. });
  10. app.start();

This can be combined with TLS for a secure authentication mechanism.

  1. $ 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.

  1. $ 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.

  1. import mojo, {jsonConfigPlugin} from '@mojojs/core';
  2. const app = mojo();
  3. app.plugin(jsonConfigPlugin);
  4. app.get('/', async ctx => {
  5. await ctx.render({json: {name: ctx.config.name}});
  6. });
  7. 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.

  1. $ mkdir plugins
  2. $ touch plugins/my-helpers.js

They work just like normal plugins.

  1. export default function myHelpersPlugin (app) {
  2. app.addHelper('renderWithHeader', async (ctx, ...args) => {
  3. ctx.res.set('X-Mojo', 'I <3 mojo.js!');
  4. await ctx.render(...args);
  5. });
  6. }

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.

  1. import mojo from '@mojojs/core';
  2. import myHelpersPlugin from './plugins/my-helpers.js';
  3. const app = mojo();
  4. app.plugin(myHelpersPlugin);
  5. app.get('/', async ctx => {
  6. await ctx.renderWithHeader({text: 'I ♥ mojo.js!'});
  7. });
  8. 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?

  1. $ mkdir cli
  2. $ touch cli/spy.js

Every command is async and has full access to the application object app.

  1. export default async function spyCommand(app, args) {
  2. const subCommand = args[2];
  3. if (subCommand === 'secrets') {
  4. console.warn(app.secrets);
  5. } else if (subCommand === 'mode') {
  6. console.warn(app.mode);
  7. }
  8. }
  9. spyCommand.description = 'Spy on application';
  10. spyCommand.usage = `Usage: APPLICATION spy [OPTIONS]
  11. node index.js spy
  12. Options:
  13. -h, --help Show this summary of available options
  14. `;

Command line arguments are passed right through and you can parse them with whatever module you prefer.

  1. $ node index.js spy secrets
  2. ["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.

  1. $ node index.js eval 'console.log(app.static.publicPaths)'
  2. ["/home/sri/myapp/public"]

The verbose option will automatically print the return value or returned data structure to STDOUT.

  1. $ node index.js eval -v 'app.renderer.viewPaths'
  2. ["/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.