Introduction

A quick example-driven introduction to the wonders of mojo.js.

Two Variants

At its heart mojo.js is an MVC framework, loosely following the architectural pattern. That means it wants you to cleanly separate the parts of your web application into Models, Views and Controllers. On the file system that is reflected as separate directories and files for different concerns.

  1. `--blog
  2. |-- controllers
  3. | |-- users.js
  4. | `-- posts.js
  5. |-- models
  6. | |-- users.js
  7. | `-- posts.js
  8. |-- public
  9. | `-- app.css
  10. |-- views
  11. | |--layouts
  12. | | `-- default.html.tmpl
  13. | `-- posts
  14. | `-- list.html.tmpl
  15. |-- config.json
  16. `-- index.js

The .js files (or .ts if you’re using TypeScript) can also be moved into a src dist, or lib directory to help with transpiling. What these files actually look like we will cover in detail later on in another guide. For now it is just important for you to know that this is considered the ideal structure for a mojo.js application. Because for the remainder of this guide we will be using a second variant.

  1. `-- blog.js

For tasks like prototyping and documentantion examples, clean abstraction with many different files can be a little distracting. So mojo.js can also be used for single file applications. And these single file apps can later on smoothly transition to proper MVC abstraction as they grow. This is one of the fundamental mojo.js design philosophies.

Installation

All you need to get started with mojo.js is Node.js 16.0.0 (or newer). We do recommend the use of an nvm environment though.

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

Be aware that mojo.js uses ES modules, so your package.json should include a "type": "module". Or you have to use the .mjs file extension instead of .js.

  1. {
  2. "type": "module",
  3. "dependencies": {
  4. "@mojojs/core": "^1.0.0"
  5. }
  6. }

Hello World

A simple Hello World application looks like this. Save it into a file myapp.js and you already got a fully functional web application. The whole framework was specifically designed with async/await in mind, so almost everything returns a Promise.

  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();

There is also a helper command available to generate a small example application for you.

  1. $ npx mojo create-lite-app
  2. ...

Commands

Many different commands are automatically available from the command line.

  1. $ node myapp.js server
  2. [39028] Web application available at http://127.0.0.1:3000/
  3. $ node myapp.js server -l http://*:8080
  4. [39029] Web application available at http://127.0.0.1:8080/
  5. $ node myapp.js get /
  6. Hello World!
  7. $ node myapp.js --help
  8. ...List of available commands...
  9. $ node myapp.js server --help
  10. ...List of available options for server command...

The app.start() call, which is usually the last statement in your application, starts the command system.

Reloading

During development you don’t want to restart your web server after every change, so we recommend the use of nodemon.

  1. $ npm install nodemon
  2. ...
  3. $ npx nodemon myapp.js server
  4. ...
  5. [39248] Web application available at http://127.0.0.1:3000/

Routes

Routes are basically just fancy paths that can contain different kinds of placeholders and usually lead to an action, if they match the path part of the request URL. The first argument passed to all actions (ctx) is a context object, containing both the HTTP request and response.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Route leading to an action that renders some text
  4. app.get('/foo', async ctx => {
  5. await ctx.render({text: 'Hello World!'});
  6. });
  7. app.start();

Response content is almost always generated by actions with a ctx.render() call, but more about that later.

GET and POST Parameters

All GET and POST parameters sent with the request are accessible via ctx.params(), which returns a Promise that resolves with a URLSearchParams object.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // GET /foo?user=sri
  4. app.get('/foo', async ctx => {
  5. const params = await ctx.params();
  6. const user = params.get('user');
  7. await ctx.render({text: `Hello ${user}.`});
  8. });
  9. app.start();

And for a little more control there are also methods to retrieve parameters separately.

  1. // Query parameters
  2. const params = ctx.req.query();
  3. // "application/x-www-form-urlencoded" or "multipart/form-data"
  4. const params = await ctx.req.form();

Stash and Views

ctx.stash is a plain object you can use to pass along arbitrary information. It is used primarily for data to be included in the output of views. And while views can be inlined for single file apps, they are usually kept as separate files in a views directory.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Route leading to an action that renders a view
  4. app.get('/foo', async ctx => {
  5. ctx.stash.one = 23;
  6. await ctx.render({inline: magicTemplate}, {two: 24});
  7. });
  8. app.start();
  9. const magicTemplate = `
  10. The magic numbers are <%= one %> and <%= two %>.
  11. `;

The default mojo.js template engine is @mojojs/template, but any other template system can be integrated, and will work just as well.

HTTP

The ctx.req and ctx.res properties of the context object give you full access to all HTTP features and information.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Access request information
  4. app.get('/agent', async ctx => {
  5. const host = ctx.req.get('Host');
  6. const ua = ctx.req.get('User-Agent');
  7. await ctx.render({text: `Request by ${ua} reached ${host}.`});
  8. });
  9. // Echo the request body and send custom header with response
  10. app.get('/agent', async ctx => {
  11. ctx.res.set('X-Bender', 'Bite my shiny metal ass!');
  12. const content = await ctx.req.text();
  13. await ctx.render({text: content});
  14. });
  15. app.start();

Take a look at the Cheatsheet to get a more complete overview of what properties and methods are available.

JSON

Of course there is first class support for JSON as well.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Modify the received JSON object and return it
  4. app.put('/add/quote', async ctx => {
  5. const data = await ctx.req.json();
  6. data.quote = "I don't remember ever fighting Godzilla... But that is so what I would have done!";
  7. await ctx.render({json: data});
  8. });
  9. app.start();

You can test all these examples right from the command line with the get command.

  1. $ node myapp.js get -X PUT -b '{"hello":"mojo"}' /add/quote

Built-in Exception and Not Found Pages

During development you will encounter these pages whenever you make a mistake, they are gorgeous and contain a lot of valuable information that will aid you in debugging your application.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Not found (404)
  4. app.get('/missing', async ctx => ctx.notFound());
  5. // Exception (500)
  6. app.get('/dies', async ctx => {
  7. throw new Error('Intentional error');
  8. });
  9. app.start();

Exception

Don’t worry about revealing too much information on these pages, they are only available during development, and will be replaced automatically with pages that don’t reveal any sensitive information in a production environment.

Exception

And of course they can be customised as well.

Route Names

All routes can have a name associated with them, this allows backreferencing with methods like ctx.urlFor(). Nameless routes get an automatically generated name assigned, based on the route pattern.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Render an inline view with links to named routes
  4. app.get('/').to(ctx => ctx.render({inline: inlineTemplate})).name('one');
  5. // Render plain text
  6. app.get('/another/page').to(ctx => ctx.render({text: 'Page two'})).name('two');
  7. app.start();
  8. const inlineTemplate = `
  9. <a href="<%= ctx.urlFor('one') %>">One</a>
  10. <a href="<%= ctx.urlFor('two') %>">Two</a>
  11. `;

Layouts

Layouts are special views that wrap around the result of another view, which is made available as ctx.content.main in the layout. Here we use the inline variant again for out single file app, but layouts are usually kept as separate files in a views/layouts directory.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Render an inline view with an inline layout
  4. app.get('/', ctx => ctx.render({inline: indexTemplate, inlineLayout: defaultLayout}, {title: 'Hello'}));
  5. app.start();
  6. const indexTemplate = `
  7. Hello World!
  8. `;
  9. const defaultLayout = `
  10. <!DOCTYPE html>
  11. <html>
  12. <head>
  13. <title><%= title %></title>
  14. </head>
  15. <body><%= ctx.content.main %></body>
  16. </html>
  17. `;

The rendering guide will cover this in much more detail.

Helpers

Helpers are little functions you can create with app.addHelper() and reuse throughout your whole application via the context (ctx), from actions to views.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // A helper to identify visitors
  4. app.addHelper('whois', ctx => {
  5. const agent = ctx.req.get('User-Agent') ?? 'Anonymous';
  6. const ip = ctx.req.ip;
  7. return `${agent} (${ip})`;
  8. });
  9. // Use helper in action and template
  10. app.get('/secret', async ctx => {
  11. const user = ctx.whois();
  12. ctx.log.debug(`Request from ${user}`);
  13. await ctx.render({inline: indexTemplate});
  14. });
  15. app.start();
  16. const indexTemplate = `
  17. We know who you are <%= ctx.whois() %>.
  18. `;

While helpers themselves can be redefined to change the behaviour of your application, they cannot overload properties inherited from the prototype chain of the context object. So core mojo.js functionality is protected.

Plugins

Plugins are application extensions that help with code sharing and organization. They are distributed as NPM modules or as part of your application. You can register plugins with app.plugin().

  1. import mojo, {jsonConfigPlugin} from '@mojojs/core';
  2. // Create application with default configuration
  3. const app = mojo({config: {foo: 'default value'}});
  4. app.plugin(jsonConfigPlugin, {file: 'myapp.conf'});
  5. // Return configured foo value
  6. app.get('/foo', async ctx => {
  7. const foo = ctx.config.foo;
  8. await ctx.render({json: {foo}});
  9. });
  10. app.start();

Now if you create a myapp.conf file in the same directory as your application, you can change the default value.

  1. {
  2. "foo": "another value"
  3. }

jsonConfigPlugin is a built-in plugin that ships with mojo.js and which can populate app.config using a config file (config.json by default). For multiple config files you can register it more than once. Plugins can also set up routes, hooks, helpers, template engines and many many other things we will later explore in the plugin guide.

Placeholders

Route placeholders allow capturing parts of a request path until a / or . separator occurs, similar to the regular expression ([^/.]+). Results are accessible via ctx.stash.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // GET /foo/test
  4. // GET /foo/test123
  5. app.get('/foo/:bar', async ctx => {
  6. const bar = ctx.stash.bar;
  7. await ctx.render({text: `Our :bar placeholder matched ${bar}.`});
  8. });
  9. // GET /testsomething/foo
  10. // GET /test123something/foo
  11. app.get('/<:bar>something/foo', async ctx => {
  12. const bar = ctx.stash.bar;
  13. await ctx.render({text: `Our :bar placeholder matched ${bar}.`});
  14. });
  15. app.start();

To separate them from the surrounding text, you can surround your placeholders with < and >, which also makes the colon prefix optional.

Relaxed Placeholders

Relaxed placeholders allow matching of everything until a / occurs, similar to the regular expression ([^/]+).

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // GET /hello/test
  4. // GET /hello/test.html
  5. app.get('/hello/#you', async ctx => {
  6. const you = ctx.stash.you;
  7. await ctx.render({text: `Your name is ${you}.`});
  8. });
  9. app.start();

Wildcard Placeholders

Wildcard placeholders allow matching absolutely everything, including / and ., similar to the regular expression (.+).

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // GET /hello/test
  4. // GET /hello/test123
  5. // GET /hello/test.123/test/123
  6. app.get('/hello/*you', async ctx => {
  7. const you = ctx.stash.you;
  8. await ctx.render({text: `Your name is ${you}.`});
  9. });
  10. app.start();

HTTP Methods

Routes can be restricted to specific request methods with different methods like app.get(), app.post() and app.any().

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // GET /hello
  4. app.get('/hello', async ctx => {
  5. await ctx.render({text: 'Hello World!'});
  6. });
  7. // PUT /hello
  8. app.get('/hello', async ctx => {
  9. const size = Buffer.byteLengt(await ctx.req.buffer());
  10. await ctx.render({text: `You uploaded ${size} bytes to /hello.`});
  11. });
  12. // GET|POST|PATCH /bye
  13. app.any(['GET', 'POST', 'PATCH'], '/bye', async ctx => {
  14. await ctx.render({text: 'Bye World!'});
  15. });
  16. // * /whatever
  17. app.any('/whatever', async ctx => {
  18. const method = ctx.req.method;
  19. await ctx.render({text: `You called /whatever with ${method}.`});
  20. });
  21. app.start();

Optional Placeholders

All placeholders require a value, but by assigning them default values you can make capturing optional. Methods like app.get() return a route object, which has a route.to() method that can be used to manually assign default values.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // GET /hello
  4. // GET /hello/Sara
  5. app.get('/hello/:name').to({name: 'Sebastian', day: 'Monday'}, async ctx => {
  6. const name = ctx.stash.name;
  7. const day = ctx.stash.day;
  8. await ctx.render({text: `My name is ${name} and it is ${day}.`});
  9. });
  10. app.start();

Default values that don’t belong to a placeholder simply get merged into ctx.stash all the time.

Restrictive Placeholders

A very easy way to make placeholders more restrictive are alternatives, you just make a list of possible values.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // * /test
  4. // * /123
  5. app.any('/:foo', {foo: ['test', '123']}, async ctx => {
  6. const foo = ctx.stash.foo;
  7. await ctx.render({text: `Our :foo placeholder matched ${foo}.`});
  8. });
  9. app.start();

All placeholders get compiled to a regular expression internally, this process can also be customized. Just make sure not to use ^ and $, or capturing groups (...), non-capturing groups (?:...) are fine though.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // * /1
  4. // * /123
  5. app.any('/:bar', {bar: /\d+/}, async ctx => {
  6. const bar = ctx.stash.bar;
  7. await ctx.render({text: `Our :bar placeholder matched ${bar}.`});
  8. });
  9. app.start();

You can take a closer look at all the generated regular expressions with the routes command.

  1. $ node myapp.js routes -v

Nested Routes

Routes can be nested in tree structures to organize them more efficiently and to share default values between branches. All methods for creating new routes, like app.get(), are therefore also available as route.get().

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // * /foo (cannot match on its own)
  4. const foo = app.any('/foo').to({name: 'Daniel'});
  5. // GET /foo/bar
  6. foo.get('/bar', async ctx => {
  7. const name = ctx.stash.name;
  8. await ctx.render({text: `My name is ${name}.`});
  9. });
  10. // GET /foo/baz
  11. foo.get('/baz', async ctx => {
  12. const name = ctx.stash.name;
  13. await ctx.render({text: `My name is also ${name}.`});
  14. });
  15. app.start();

Only the actual endpoints of a route can match, so a request for /foo would not yield a result.

Under

Authentication and code shared between multiple routes can be realized easily with routes created by app.under(). All nested routes are only evaluated if the action returns a value other than false.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Authenticate based on name parameter
  4. const admin = app.under('/admin', async ctx => {
  5. // Authenticated
  6. const params = await ctx.params();
  7. if (params.get('name') === 'Bender') return;
  8. // Not authenticated
  9. await ctx.render({text: 'You are not Bender, permission denied.'});
  10. return false;
  11. });
  12. // GET /admin?name=Bender
  13. admin.get('/', async ctx => {
  14. await ctx.render({text: 'Hi Bender!'});
  15. });
  16. app.start();

Extensions

File extensions can be captured as well with the special ext route placeholder. Just use a restrictive placeholder to declare possible values.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // GET /detect.html
  4. // GET /detect.txt
  5. app.get('/detect', {ext: ['html', 'txt']}, async ctx => {
  6. const ext = ctx.stash.ext;
  7. await ctx.render({text: `Detected .${ext} extension.`});
  8. });
  9. app.start();

And just like with placeholders you can use a default value to make the extension optional.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // GET /detect
  4. // GET /detect.htm
  5. // GET /detect.html
  6. app.get('/detect', {ext: /html?/}).to({ext: 'html'}, async ctx => {
  7. const ext = ctx.stash.ext;
  8. await ctx.render({text: `Detected .${ext} extension.`});
  9. });
  10. app.start();

Static Files

Static files will be served automatically from the public directory of your application if it exists. All static URLs have a /static prefix by default, to make it easier to integrate reverse proxy servers in production environments.

  1. $ mkdir public
  2. $ echo 'Hello World!' > public/test.txt

Since the prefix is configurable, ctx.urlForFile() can be used to generate the URL.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Redirect to static file
  4. app.get('/file', async ctx => {
  5. const url = ctx.urlForFile('/test.txt');
  6. await ctx.redirectTo(url, {status: 301});
  7. });
  8. app.start();

For GET and HEAD requests static files have a higher precedence than routes. Content negotiation with Range, If-None-Match and If-Modified-Since headers is supported as well, and can be tested very easily with the get command.

  1. $ node myapp.js get /test -v -H 'Range: bytes=2-4'

External Views

The renderer will seach for views in the views directory of your application if it exists. And for layouts in the views/layouts subdirectory.

  1. $ mkdir -p views/layouts
  1. %# views/hello.html.tmpl
  2. Hello <%= name %>!
  1. %# views/layouts/default.html.tmpl
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5. <title><%= title %></title>
  6. </head>
  7. <body><%= ctx.content.main %></body>
  8. </html>

All views are expected to be in the format name.format.engine, such as list.html.tmpl. The format and engine values are used to select the correct MIME type and template engine.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Render a view "views/hello.html.tmpl" with layout "views/layouts/default.html.tmpl"
  4. app.get('/', async ctx => {
  5. await ctx.render({view: 'hello', layout: 'default'}, {title: 'Hello', name: 'Isabell'});
  6. });
  7. app.start();

Validation

Instead of form validation we use JSON Schema data structure validation with ajv for everything.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // GET /form?test=13
  4. app.get('/form', async ctx => {
  5. // Prepare validation function for schema
  6. const validate = ctx.schema({
  7. $id: 'testForm',
  8. type: 'object',
  9. properties: {
  10. test: {type: 'number'}
  11. },
  12. required: ['test']
  13. });
  14. // Turn request parameters into a plain object
  15. const params = await ctx.params();
  16. const testData = params.toObject();
  17. // Validate request parameters
  18. const result = validate(testData);
  19. if (result.isValid === true) {
  20. await ctx.render({json: testData});
  21. } else {
  22. await ctx.render({json: {error: {message: 'Validation failed'}}, status: 400});
  23. }
  24. });
  25. app.start();

Just remember to include an $id value, so the validation function can be cached. Or even better, register the schema during application startup with app.validator.addSchema().

  1. // Register schema
  2. app.validator.addSchema({
  3. type: 'object',
  4. properties: {
  5. test: {type: 'number'}
  6. },
  7. required: ['test']
  8. }, 'testForm');
  9. // Request schema by name from now on
  10. const validate = ctx.schema('testForm');

Home

The directory in which the main application script resides, usually called index.js is considered the application home directory. For convenience it is available as app.home.

  1. $ mkdir cache
  2. $ echo 'Hello World!' > cache/hello.txt

The Path object provides many useful fs.* and path.* functions from Node.js, as well as some custom additions.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Load message into memory
  4. const hello = app.home.child('cache', 'hello.txt').readFileSync('utf8');
  5. // Display message
  6. app.get('/', async ctx => {
  7. await ctx.render({text: hello});
  8. });
  9. app.start();

Conditions

Conditions such as headers and host are router extensions and allow for even more powerful route constructs.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Firefox
  4. app.get('/foo').requires('headers', {'User-Agent': /Firefox/}).to(async ctx => {
  5. await ctx.render({text: 'Congratulations, you are using a cool browser.'});
  6. });
  7. // http://mojolicious.org/bar
  8. app.get('/bar').requires('host', /mojolicious\.org/).to(async ctx => {
  9. await ctx.render({text: 'Hello Mojolicious.'});
  10. });
  11. app.start();

Adding your own router exptensions will be covered later in another guide.

Sessions

Encrypted cookie based sessions just work out of the box, as soon as you start using them through ctx.session(). Just be aware that all session data gets serialized as JSON.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Access session data in action and template
  4. app.get('/counter', async ctx => {
  5. const session = await ctx.session();
  6. if (session.counter === undefined) session.counter = 0;
  7. session.counter++;
  8. await ctx.render({inline: inlineCounter}, {session});
  9. });
  10. app.start();
  11. const inlineCounter = `
  12. Counter: <%= session.counter %>
  13. `;

Note that you should use custom rotating secrets to make signed cookies really tamper resistant. Only the first secret will be used to encrypt new cookies, but all of them to decrypt existing ones.

  1. app.secrets = ['My secret passphrase here'];

File Uploads

Files uploaded with multipart/form-data requests are available via ctx.req.files(). They are not cached to disk, but instead streamed as data arrives.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. app.get('/', async ctx => ctx.render({inline: inlineForm}));
  4. app.post('/upload', async ctx => {
  5. let bytes = 0;
  6. for await (const {fieldname, file, filename} of ctx.req.files()) {
  7. ctx.log.debug(`Uploading file ${filename}`);
  8. // Files are `stream.Readable` objects
  9. for await (const chunk of file) {
  10. const size = Buffer.byteLength(chunk);
  11. bytes += size;
  12. ctx.log.debug(`${size} byte chunk uploaded`);
  13. }
  14. }
  15. await ctx.render({text: `${bytes} bytes uploaded.`});
  16. });
  17. app.start();
  18. const inlineForm = `
  19. <!DOCTYPE html>
  20. <html>
  21. <head><title>Upload</title></head>
  22. <body>
  23. <form action="<%= ctx.urlFor('upload') %>" enctype="multipart/form-data" method="POST">
  24. <input name="example" type="file">
  25. <input type="submit" value="Upload">
  26. </form>
  27. </body>
  28. </html>
  29. `;

Just be aware that if you are also using ctx.params() or ctx.req.form(), they have to be called after ctx.req.files().

User-Agent

While its primary purpose is testing, there is also a full featured HTTP and WebSocket user agent available via ctx.ua.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. app.get('/', async ctx => {
  4. const params = await ctx.params();
  5. const url = params.get('url') ?? 'https://mojolicious.org';
  6. const res = await ctx.ua.get(url);
  7. const html = await res.html();
  8. const title = html.at('title').text();
  9. await ctx.render({text: title});
  10. });
  11. app.start();

For more information take a look at the User-Agent guide.

WebSockets

WebSocket applications have never been this simple before. You can accept incoming connections with two methods, ctx.plain() to receive and send messages unaltered, and ctx.json() to have messages automatically JSON encoded and decoded.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. app.get('/', async ctx => ctx.render({inline: inlineTemplate}));
  4. app.websocket('/echo', async ctx => {
  5. ctx.json(async ws => {
  6. for await (const message of ws) {
  7. message.hello = `echo: ${message.hello}`;
  8. ws.send(message);
  9. }
  10. });
  11. });
  12. app.start();
  13. const inlineTemplate = `
  14. <!DOCTYPE html>
  15. <html>
  16. <head>
  17. <title>Echo</title>
  18. <script>
  19. var ws = new WebSocket('<%= ctx.urlFor('echo') %>');
  20. ws.onmessage = event => {
  21. document.body.innerHTML += JSON.parse(event.data).hello;
  22. };
  23. ws.onopen = event => {
  24. ws.send(JSON.stringify({hello: 'I ♥ mojo.js!'}));
  25. };
  26. </script>
  27. </head>
  28. </html>
  29. `;

Modes

Every mojo.js application has multiple operating modes, which can be selected with the NODE_ENV environment variable.

  1. NODE_ENV=production node myapp.js server

The default operating mode is development, which sets the log level of app.log and ctx.log to the lowest level (trace). All other modes raise the level to info.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. // Prepare mode specific message during startup
  4. const msg = app.mode === 'development' ? 'Development!' : 'Something else!';
  5. app.get('/', async ctx => {
  6. ctx.log.trace('Rendering mode specific message');
  7. await ctx.render({text: msg});
  8. });
  9. app.log.trace('Starting application');
  10. app.start();

Mode changes also affect a few other aspects of the framework, such as the built-in exception and not_found pages. Once you switch modes from development to production, no sensitive information will be revealed on those pages anymore.

Testing

Testing you mojo.js application is as easy as creating a test directory and filling it with normal JavaScript tests like test/basic.js. Especially if you use tap and the built-in test user agent.

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

Just make sure to export app from your application script.

  1. import {app} from '../myapp.js';
  2. import t from 'tap';
  3. t.test('Basics', async t => {
  4. const ua = await app.newTestUserAgent({tap: t});
  5. await t.test('Index', async t => {
  6. (await ua.getOk('/')).statusIs(200).bodyLike(/mojo.js/);
  7. });
  8. await ua.stop();
  9. });

And run your tests as scripts or with tap.

  1. $ node test/basic.js
  2. $ tap --no-coverage test/*.js

For more information take a look at the User-Agent guide.

Support

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