Growing

This document explains the process of starting a single file prototype from scratch and growing it into a well-structured mojo.js application.

Concepts

Essentials every mojo.js developer should know.

Model View Controller

MVC is a software architectural pattern for graphical user interface programming originating in Smalltalk-80, that separates application logic, presentation and input.

  1. +------------+ +-------+ +------+
  2. Input -> | Controller | -> | Model | -> | View | -> Output
  3. +------------+ +-------+ +------+

A slightly modified version of the pattern moving some application logic into the controller is the foundation of pretty much every web framework these days, including mojo.js.

  1. +----------------+ +-------+
  2. Request -> | | <-> | Model |
  3. | | +-------+
  4. | Controller |
  5. | | +-------+
  6. Response <- | | <-> | View |
  7. +----------------+ +-------+

The controller receives a request from a user, passes incoming data to the model and retrieves data from it, which then gets turned into an actual response by the view. But note that this pattern is just a guideline that most of the time results in cleaner more maintainable code, not a rule that should be followed at all costs.

REpresentational State Transfer

REST is a software architectural style for distributed hypermedia systems such as the web. While it can be applied to many protocols it is most commonly used with HTTP these days. In REST terms, when you are opening a URL like http://mojojs.org/foo with your browser, you are basically asking the web server for the HTML representation of the http://mojojs.org/foo resource.

  1. +--------+ +--------+
  2. | | -> http://mojojs.org/foo -> | |
  3. | Client | | Server |
  4. | | <- <html>Mojo rocks!</html> <- | |
  5. +--------+ +--------+

The fundamental idea here is that all resources are uniquely addressable with URLs and every resource can have different representations such as HTML, RSS or JSON. User interface concerns are separated from data storage concerns and all session state is kept client-side.

  1. +---------+ +------------+
  2. | | -> PUT /foo -> | |
  3. | | -> Hello World! -> | |
  4. | | | |
  5. | | <- 201 CREATED <- | |
  6. | | | |
  7. | | -> GET /foo -> | |
  8. | Browser | | Web Server |
  9. | | <- 200 OK <- | |
  10. | | <- Hello World! <- | |
  11. | | | |
  12. | | -> DELETE /foo -> | |
  13. | | | |
  14. | | <- 200 OK <- | |
  15. +---------+ +------------+

While HTTP methods such as PUT, GET and DELETE are not directly part of REST they go well with it and are commonly used to manipulate resources.

Sessions

HTTP was designed as a stateless protocol, web servers don’t know anything about previous requests, which makes user-friendly login systems tricky. Sessions solve this problem by allowing web applications to keep stateful information across several HTTP requests.

  1. GET /login?user=sebastian&pass=s3cret HTTP/1.1
  2. Host: mojojs.org
  3. HTTP/1.1 200 OK
  4. Set-Cookie: sessionid=987654321
  5. Content-Length: 10
  6. Hello sebastian.
  7. GET /protected HTTP/1.1
  8. Host: mojojs.org
  9. Cookie: sessionid=987654321
  10. HTTP/1.1 200 OK
  11. Set-Cookie: sessionid=987654321
  12. Content-Length: 16
  13. Hello again sebastian.

Traditionally all session data was stored on the server-side and only session IDs were exchanged between browser and web server in the form of cookies.

  1. Set-Cookie: session=aes-256-gcm(json({user: 'sebastian'}))

In mojo.js however we are taking this concept one step further by storing everything JSON serialized in AES-256-GCM encrypted cookies, which is more compatible with the REST philosophy and reduces infrastructure requirements.

Test-Driven Development

TDD is a software development process where the developer starts writing failing test cases that define the desired functionality and then moves on to producing code that passes these tests. There are many advantages such as always having good test coverage and code being designed for testability, which will in turn often prevent future changes from breaking old code. Much of mojo.js was developed using TDD.

Prototype

An important difference between mojo.js and other web frameworks is that it can operate in two modes, both as a full-fledged web framework, and as a single file micro web framework optimized for rapid prototyping.

Differences

You likely know the feeling, you’ve got a really cool idea and want to try it as quickly as possible. That’s exactly why mojo.js applications don’t need more than a single JavaScript file (in addition to package.json).

  1. myapp // Application directory (created manually)
  2. |-- node_modules/
  3. | `-- *lots of node files*
  4. |-- package.json // Will be generated when you install mojo.js
  5. `-- myapp.js // Templates can be inlined in the file

Full mojo.js applications on the other hand follow the MVC pattern more closely and separate concerns into different files to maximize maintainability:

  1. myapp // Application directory (created manually)
  2. |-- node_modules
  3. | `-- *lots of node files*
  4. |-- package.json // Node package information and settings
  5. |-- test // Test directory
  6. | `-- example.js // Random test
  7. |-- config.yml // Configuration file
  8. |-- public // Static file directory (served automatically)
  9. | `-- index.html // Static HTML file
  10. |-- index.js // Application script
  11. |-- controllers // Controller directory
  12. | `-- example.js // Controller class
  13. |-- models // Model directory
  14. `-- views // Views directory
  15. |-- example // View directory for "example" controller
  16. | `-- welcome.html.tmpl // Template for "welcome" action
  17. `-- layouts // View directory for layout templates
  18. `-- default.html.tmpl // Layout template

Both application skeletons can be automatically generated with the commands npx mojo create-lite-app and npx mojo create-full-app.

  1. $ mkdir myapp && cd myapp
  2. $ npm install @mojojs/core
  3. $ npx mojo create-full-app # or
  4. $ npx mojo create-lite-app

Feature-wise both are almost equal, the only real differences are organizational, so each one can be gradually transformed into the other.

TypeScript

TypeScript is fully supported as well, and in fact mojo.js itself is written entirely in TypeScript. But because it requires a build step, we recommend a slightly different directory layout for applications that are planning to use it. With a src directory for .ts source files, and a lib directory for the compiled .js output files.

  1. myapp // Application directory (created manually)
  2. |-- node_modules
  3. | `-- *lots of node files*
  4. |-- package.json // Node package information and settings
  5. |-- tsconfig.json // TypeScript configuration
  6. |-- test // Test directory
  7. | `-- example.js // Random test
  8. |-- config.yml // Configuration file
  9. |-- public // Static file directory (served automatically)
  10. | `-- index.html // Static HTML file
  11. |-- src // TypeScript source directory
  12. | |-- index.ts // Application script
  13. | |-- controllers // Controller directory
  14. | | `-- example.ts // Controller class
  15. | `-- models // Model directory
  16. |-- lib
  17. | `-- *compiled js files*
  18. `-- views // Views directory
  19. |-- example // View directory for "example" controller
  20. | `-- welcome.html.tmpl // Template for "welcome" action
  21. `-- layouts // View directory for layout templates
  22. `-- default.html.tmpl // Layout template

A fully functional TypeScript mojo.js application can be generated with the command npx mojo create-full-app --ts.

  1. $ mkdir myapp && cd myapp
  2. $ npm install @mojojs/core
  3. $ npx mojo create-full-app --ts
  4. $ npm install
  5. $ npm run build:test

However, the use of TypeScript is completely optional, and for the rest if this guide we will stick with plain old JavaScript.

Foundation

We start our new application with a single JavaScript file.

  1. $ mkdir myapp
  2. $ cd myapp
  3. $ npm install @mojojs/core
  4. $ touch myapp.js

This will be the foundation for our login manager example application.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. app.get('/', async ctx => {
  4. await ctx.render({text: 'Hello World!'})
  5. });
  6. app.start();

Use the built-in server command to start a development web server with node.

  1. $ node myapp.js server
  2. Web application available at http://0.0.0.0:3000/

For a little more convenice, we recommend nodemon, which can watch files for changes and automatically restart the web server for you.

  1. $ npm install nodemon
  2. $ npx nodemon myapp.js server

A Bird’s-Eye View

It all starts with an HTTP request like this, sent by your browser.

  1. GET / HTTP/1.1
  2. Host: localhost:3000

Once the request has been received by the web server through the event loop, it will be passed on to mojo.js, where it will be handled in a few simple steps.

  1. Check if a static file exists that would meet the requirements.
  2. Try to find a route that would meet the requirements.
  3. Dispatch the request to this route, usually reaching one or more actions.
  4. Process the request, maybe generating a response with the renderer.
  5. Return control to the web server, and if no response has been generated yet, wait for a non-blocking operation to do so through the event loop.

With our application the router would have found an action in step 2, and rendered some text in step 4, resulting in an HTTP response like this being sent back to the browser.

  1. HTTP/1.1 200 OK
  2. Content-Type: text/plain; charset=utf-8
  3. Content-Length: 12
  4. Date: Wed, 15 Dec 2021 22:47:21 GMT
  5. Connection: keep-alive
  6. Keep-Alive: timeout=5
  7. Hello World!

Model

In mojo.js we consider web applications simple frontends for existing business logic. That means mojo.js is by design entirely model layer agnostic, and you just use whatever JavaScript modules you like most.

  1. $ mkdir models
  2. $ touch models/users.js

Our login manager will use a JavaScript class abstracting away all logic related to matching usernames and passwords. The path models/users.js is an arbitrary choice, and is simply used to make the separation of concerns more visible.

  1. export default class Users {
  2. constructor() {
  3. this._data = {
  4. joel: 'las3rs',
  5. marcus: 'lulz',
  6. sebastian: 'secr3t'
  7. };
  8. }
  9. check(user, pass) {
  10. if(this._data[user] === undefined) return false;
  11. return this._data[user] === pass;
  12. }
  13. }

We can add the model to the app to make it available to all actions and templates.

  1. import mojo from '@mojojs/core';
  2. import Users from './models/users.js';
  3. export const app = mojo();
  4. app.models.users = new Users();
  5. app.any('/', async ctx => {
  6. // Query or POST parameters
  7. const params = await ctx.params();
  8. const user = params.get('user')
  9. const pass = params.get('pass')
  10. // Check password
  11. if(ctx.models.users.check(user, pass) === true) return await ctx.render({text: `Welcome ${user}.`});
  12. // Failed
  13. return await ctx.render({text: 'Wrong username or password.'});
  14. });
  15. app.start();

The method params is used to access both query parameters and POST parameters. It returns a Promise that resolves with a URLSearchParams object.

Testing

In mojo.js we take testing very seriously and try to make it a pleasant experience.

  1. $ mkdir tests
  2. $ touch tests/login.js

TestUserAgent is a scriptable HTTP user agent designed specifically for testing, with many fun and state-of-the-art features such as CSS selectors based on @mojojs/dom.

  1. import {app} from '../myapp.js';
  2. import t from 'tap';
  3. t.test('Example application', async t => {
  4. const ua = await app.newTestUserAgent({tap: t, maxRedirects: 1});
  5. await t.test('Index', async t => {
  6. (await ua.getOk('/'))
  7. .statusIs(200)
  8. .elementExists('form input[name="user"]')
  9. .elementExists('form input[name="pass"]')
  10. .elementExists('button[type="submit"]');
  11. (await ua.postOk('/', {form: {user: 'sebastian', pass: 'secr3t'}}))
  12. .statusIs(200).textLike('html body', /Welcome sebastian/);
  13. // Test accessing a protected page
  14. (await ua.getOk('/protected')).statusIs(200).textLike('a', /Logout/);
  15. // Test if HTML login form shows up again after logout
  16. (await ua.getOk('/logout'))
  17. .statusIs(200)
  18. .elementExists('form input[name="user"]')
  19. .elementExists('form input[name="pass"]')
  20. .elementExists('button[type="submit"]');
  21. });
  22. await ua.stop();
  23. });

Your application won’t pass these tests, but from now on you can use them to check your progress.

  1. $ node tests/login.t
  2. ...

Or perform quick requests right from the command line with the get command.

  1. $ node myapp.pl get /
  2. Wrong username or password.
  3. $ node myapp.js get -v '/?user=sebastian&pass=secr3t'
  4. [2021-12-22T19:06:06.688Z] [trace] [16173-000001] GET "/"
  5. [2021-12-22T19:06:06.688Z] [trace] [16173-000001] Routing to function
  6. [2021-12-22T19:06:06.689Z] [trace] [16173-000001] Rendering text response
  7. GET /?user=sebastian&pass=secr3t HTTP/1.1
  8. Accept-Encoding: gzip
  9. Host: 0.0.0.0:55841
  10. HTTP/1.1 200 OK
  11. Content-Type: text/plain; charset=utf-8
  12. Content-Length: 18
  13. Date: Wed, 22 Dec 2021 19:06:06 GMT
  14. Connection: close
  15. Welcome sebastian.

State Keeping

Sessions in mojo.js pretty much just work out-of-the-box once you await the session method, there is no setup required, but we suggest setting a more secure passphrase with app.secrets.

  1. app.secrets = ['Mojolicious rocks'];

This passphrase is used by the AES-256-GCM algorithm to encrypt cookies and can be changed at any time to invalidate all existing sessions.

  1. const session = await ctx.session();
  2. session.user = 'sebastian';
  3. const user = session.user;

By default, all sessions expire after one hour. For more control you can use the expiration session value to set an expiration date in seconds from now.

  1. const session = await ctx.session();
  2. session.expiration = 3600;

And the whole session can be deleted by using the expires session value to set an absolute expiration date in the past.

  1. session.expires = 1;

For data that should only be visible on the next request, like a confirmation message after a 302 redirect performed with ctx.redirectTo(), you can use the flash, accessible through ctx.flash().

  1. const flash = await ctx.flash();
  2. flash.message = 'Everything is fine.';
  3. await ctx.redirectTo('goodbye');

Just remember that all session data gets serialized to JSON and stored in encrypted cookies, which usually have a 4096 byte (4KiB) limit, depending on browser.

Final Prototype

A final myapp.js prototype passing all of the tests above could look like this.

  1. import mojo from '@mojojs/core';
  2. import Users from './models/users.js';
  3. // Set custom cookie secret to ensure encryption is more secure
  4. export const app = mojo({secrets: ['Mojolicious rocks']});
  5. app.models.users = new Users();
  6. // Main login action
  7. app.any('/', async ctx => {
  8. // Query or POST parameters
  9. const params = await ctx.params();
  10. const user = params.get('user');
  11. const pass = params.get('pass');
  12. // Check password and render the index inline template if necessary
  13. if (ctx.models.users.check(user, pass) === false) {
  14. return await ctx.render({inline: indexTemplate, inlineLayout: defaultLayout});
  15. }
  16. // Store username in session
  17. const session = await ctx.session();
  18. session.user = user;
  19. // Store a friendly message for the next page in flash
  20. const flash = await ctx.flash();
  21. flash.message = 'Thanks for logging in.';
  22. // Redirect to protected page with a 302 response
  23. await ctx.redirectTo('protected');
  24. }).name('index');
  25. // Make sure user is logged in for actions in this action
  26. const loggedIn = app.under('/').to(async ctx => {
  27. // Redirect to main page with a 302 response if user is not logged in
  28. const session = await ctx.session();
  29. if (session.user !== undefined) return;
  30. await ctx.redirectTo('index');
  31. return false;
  32. });
  33. // A protected page auto rendering the protected inline template"
  34. loggedIn.get('/protected').to(async ctx => {
  35. await ctx.render({inline: protectedTemplate, inlineLayout: defaultLayout});
  36. });
  37. // Logout action
  38. app.get('/logout', async ctx => {
  39. // Expire and in turn clear session automatically
  40. const session = await ctx.session();
  41. session.expires = 1;
  42. // Redirect to main page with a 302 response
  43. await ctx.redirectTo('index');
  44. });
  45. app.start();
  46. const indexTemplate = `
  47. % const params = await ctx.params();
  48. <form method="post">
  49. % if (params.user !== null) {
  50. <b>Wrong name or password, please try again.</b><br>
  51. % }
  52. User:<br>
  53. <input name="user">
  54. <br>Password:<br>
  55. <input type="password" name="pass">
  56. <br>
  57. <button type="submit">Log in</button>
  58. </form>
  59. `;
  60. const protectedTemplate = `
  61. % const flash = await ctx.flash();
  62. % if (flash.message != null) {
  63. <b><%= flash.message %></b><br>
  64. % }
  65. Welcome <%= ctx.stash.session.user %>.<br>
  66. %= ctx.linkTo('logout', {}, 'Logout')
  67. `;
  68. const defaultLayout = `
  69. <!DOCTYPE html>
  70. <html>
  71. <head><title>Login Manager</title></head>
  72. <body><%== ctx.content.main %></body>
  73. </html>
  74. `;

And the directory structure should be looking like this now.

  1. myapp
  2. |-- myapp.js
  3. |-- models
  4. | `-- users.js
  5. `-- tests
  6. `-- login.js

Our templates are using quite a few features of the renderer, the Rendering guide explains them all in great detail.

Well-Structured Application

Due to the flexibility of mojo.js, there are many variations of the actual growing process, but this should give you a good overview of the possibilities.

Moving Templates

While inline templates are great for prototyping, later on it is much easier to manage a growing number of templates as separate files in the views directory.

  1. $ mkdir -p views/layouts
  2. $ touch views/layouts/default.html.tmpl
  3. $ touch views/index.html.tmpl
  4. $ touch views/protected.html.tmpl

Just move the content of the indexTemplate, protectedTemplate and defaultLayout constants into those template files. Instead of selecting a layout in the ctx.render() call, from now on we will let each template select it for themselves, so we have to add a view.layout statement (as first line) to each of them.

  1. % view.layout = 'default';
  2. ...rest of the template...

Simplified Application

Next we need to update all ctx.render() calls and remove the inline templates from our application.

  1. import mojo from '@mojojs/core';
  2. import Users from './models/users.js';
  3. export const app = mojo({secrets: ['Mojolicious rocks']});
  4. app.models.users = new Users();
  5. app.any('/', async ctx => {
  6. const params = await ctx.params();
  7. const user = params.get('user');
  8. const pass = params.get('pass');
  9. if (ctx.models.users.check(user, pass) === false) return await ctx.render({view: 'index'});
  10. const session = await ctx.session();
  11. session.user = user;
  12. const flash = await ctx.flash();
  13. flash.message = 'Thanks for logging in.';
  14. await ctx.redirectTo('protected');
  15. }).name('index');
  16. const loggedIn = app.under('/').to(async ctx => {
  17. const session = await ctx.session();
  18. if (session.user !== undefined) return;
  19. await ctx.redirectTo('index');
  20. return false;
  21. });
  22. loggedIn.get('/protected').to(async ctx => {
  23. await ctx.render({view: 'protected'});
  24. });
  25. app.get('/logout', async ctx => {
  26. const session = await ctx.session();
  27. session.expires = 1;
  28. await ctx.redirectTo('index');
  29. });
  30. app.start();

And the directory structure of our hybrid application should be looking like this.

  1. myapp
  2. |-- myapp.js
  3. |-- models
  4. | `-- users.js
  5. |-- tests
  6. | `-- login.js
  7. `-- views
  8. |-- layouts
  9. | `-- default.html.tmpl
  10. |-- index.html.tmpl
  11. `-- protected.html.tmpl

The tests will work again now.

Controller Class

Hybrid routes with separate template files are a nice intermediate step, but to maximize maintainability it makes sense to split our action code from its routing information.

  1. $ mkdir controllers
  2. $ touch controlers/login.js

Once again the actual action code does not need to change much, we just turn them into methods and remove the arguments from the ctx.render() calls (because from now on we will rely on default controller/action template names).

  1. export default class LoginController {
  2. async index(ctx) {
  3. const params = await ctx.params();
  4. const user = params.get('user');
  5. const pass = params.get('pass');
  6. if (ctx.models.users.check(user, pass) === false) return await ctx.render();
  7. const session = await ctx.session();
  8. session.user = user;
  9. const flash = await ctx.flash();
  10. flash.message = 'Thanks for logging in.';
  11. await ctx.redirectTo('protected');
  12. }
  13. async loggedIn(ctx) {
  14. const session = await ctx.session();
  15. if (session.user !== undefined) return;
  16. await ctx.redirectTo('index');
  17. return false;
  18. }
  19. async protected(ctx) {
  20. await ctx.render();
  21. }
  22. async logout(ctx) {
  23. const session = await ctx.session();
  24. session.expires = 1;
  25. await ctx.redirectTo('index');
  26. }
  27. }

All mojo.js controllers are just ES6 classes and get instantiated on demand by the router.

Final Application

The application script myapp.js can now be reduced to model and routing information.

  1. import mojo from '@mojojs/core';
  2. import Users from './models/users.js';
  3. export const app = mojo({secrets: ['Mojolicious rocks']});
  4. app.models.users = new Users();
  5. app.any('/').to('login#index').name('index');
  6. app.get('/logout').to('login#logout');
  7. const loggedIn = app.under('/').to('login#loggedIn');
  8. loggedIn.get('/protected').to('login#protected');
  9. app.start();

The router allows many different route variations, the Routing guide explains them all in great detail.

Templates

Templates are our views, and usually bound to controllers, so they need to be moved into the appropriate directories.

  1. $ mkdir views/login
  2. $ mv views/index.html.tmpl views/login/index.html.tmpl
  3. $ mv views/protected.html.tmpl views/login/protected.html.tmpl

Now the tests will work again and our final directory structure should be looking like this.

  1. myapp
  2. |-- myapp.js
  3. |-- controllers
  4. | `-- login.js
  5. |-- models
  6. | `-- users.js
  7. |-- tests
  8. | `-- login.js
  9. `-- views
  10. |-- layouts
  11. | `-- default.html.tmpl
  12. `-- login
  13. |-- index.html.tmpl
  14. `-- protected.html.tmpl

Test-driven development takes a little getting used to, but can be a very powerful tool.

Support

If you have any questions the documentation might not yet answer, don’t hesitate to ask in the Forum, on Matrix, or IRC.