πŸ”

WEBSITE

This folder only contains the website: server website

You might be looking for the folder "Documentation": documentation folder

Which can also be accessed through the website: documentation in the website

Note: I've contacted Github about how ridiculous it is to have to name the web as docs and suggested that they allow the name... web. They answered quickly and said it was in their tasklog, so kudos :+1:.

Documentation

Conceptually server is a function that accepts options and other functions. The heavy lifting is already implemented so you can focus on your project:

// Import the variable into the file
const server = require('server');

// All of the arguments are optional
server(options, fn1, fn2, fn3, ...);

You can also learn Node.js development by following the tutorials.

Getting started

There's a getting started tutorial for beginners. If you know your way around:

npm install server

Then create some demo code in your index.js:

// Import the library
const server = require('server');

// Answers to any request
server(ctx => 'Hello world');

Run it from the terminal:

node .

And open your browser on localhost:3000 to see it in action.

Basic usage

Some of the components are the main function on itself, router and reply. The main function accepts first an optional object for the options, and then as many middleware or arrays of middleware as wanted:

const server = require('server');

server({ port: 3000 }, ctx => 'Hello δΈ–η•Œ');

To use the router and reply extract their methods as needed:

const server = require('server');
const { get, post } = server.router;
const { render, json } = server.reply;

server([
  get('/', ctx => render('index.hbs')),
  post('/', ctx => json(ctx.data)),
  get(ctx => status(404))
]);

Then when you are splitting your files into different parts and don't have access to the global server you can import only the corresponding parts:

const { get, post } = require('server/router');
const { render, json } = require('server/reply');

Middleware

A middleware is plain function that will be called on each request. It receives a context object and returns a reply, a basic type or nothing. A couple of examples:

const setname = ctx => { ctx.user = 'Francisco'; };
const sendname = ctx => send(ctx.user);
server(setname, sendname);

They can be placed as server() arguments, combined into an array or imported/exported from other files:

server(
  ctx => send(ctx.user),
  [ ctx => console.log(ctx.data) ],
  require('./comments/router.js')
);

Then in ./comments/router.js:

const { get, post, put, del } = require('server/router');
const { json } = require('server/reply');

module.exports = [
  get('/',    ctx => { /* ... */ }),
  post('/',   ctx => { /* ... */ }),
  put('/:id', ctx => { /* ... */ }),
  del('/:id', ctx => { /* ... */ }),
];

The main difference between synchronous and asynchronous functions is that you use async keyword to then be able to use the keyword await within the function, avoiding callback hell. Some examples of middleware:

// Some simple logging
const mid = () => {
  console.log('Hello δΈ–η•Œ');
};

// Asynchronous, find user with Mongoose (MongoDB)
const mid = async ctx => {
  ctx.user = await User.find({ name: 'Francisco' }).exec();
  console.log(ctx.user);
};

// Make sure that there is a user
const mid = ctx => {
  if (!ctx.user) {
    throw new Error('No user detected!');
  }
};

// Send some info to the browser
const mid = ctx => {
  return `Some info for ${ctx.user.name}`;
};

In this way you can await inside of your function. Server.js will also await to your middleware before proceeding to the next one:

server(async ctx => {
  await someAsyncOperation();
  console.log('I am first');
}, ctx => {
  console.log('I am second');
});

If you find an error in an async function you can throw it. It will be caught, a 500 error will be displayed to the user and the error will be logged:

const middle = async ctx => {
  if (!ctx.user) {
    throw new Error('No user :(');
  }
};
**Avoid callback-based functions**: error propagation is problematic and they have to be converted to promises. Strongly prefer an async/await workflow.

Express middleware

Server.js is using express as the underlying library (we <3 express!). You can import middleware designed for express with modern:

const server = require('server');

// Require it and initialize it with some options
const legacy = require('helmet')({ ... });

// Convert it to server.js middleware
const mid = server.utils.modern(legacy);

// Add it as you'd add a normal middleware
server(mid, ...);

Note: the { ... } represent the options for that middleware since many of express libraries follow the factory pattern.

To simplify it, we can also perform this operation inline:

const server = require('server');
const { modern } = server.utils;

server(
  modern(require('express-mid-1')({ ... })),
  modern(require('express-mid-2')({ ... })),
  // ...
);

Or just keep the whole middleware in a separated file/folder:

// index.js
const server = require('server');
const middleware = require('./middleware');
const routes = require('./routes');

server(middleware, routes);

Then in our middleware.js:

// middleware.js
const server = require('server');
const { modern } = server.utils;

module.exports = [
  modern(require('express-mid-1')({ /* ... */ })),
  modern(require('express-mid-2')({ /* ... */ }))
];

Read the next section for a great example of a common middleware from express used with server.

CORS

To allow requesting a resource from another domain you must enable Cross-Origin Resource Sharing (CORS). To do so, you have two options: do it manually or through a great library. Both of them end up setting some headers.

Let's see how to do it manually for any domain:

const server = require('server');
const { header } = server.reply;  // OR server.reply;

const cors = [
  ctx => header("Access-Control-Allow-Origin", "*"),
  ctx => header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"),
  ctx => header("Access-Control-Allow-Methods", "GET, PUT, PATCH, POST, DELETE, HEAD"),
  ctx => ctx.method.toLowerCase() === 'options' ? 200 : false
];

server({}, cors, ...);

If you want to whitelist some domains it's not easy manually, so we can use the great package cors from npm:

const server = require('server');

// Load it with the options
const corsExpress = require('cors')({
  origin: ['https://example.com', 'https://example2.com']
});

// Make the express middleware compatible with server
const cors = server.utils.modern(corsExpress);

// Launch the server with this specific middleware
server({}, cors, ...);

Routing

This is the concept of redirecting each request to our server to the right place. For instance, if the user requests our homepage / we want to render the homepage, but if they request an image gallery /gallery/67546 we want to render the gallery 67546.

For this we will be creating routes using server's routers. We can import it like this:

const server = require('server');
const { get, post } = server.router;

// OR

const { get, post } = require('server/router');

There are some other ways, but these are the recommended ones. Then we say the path of the request for the method that we want to listen to and a middleware:

const getHome = get('/', () => render('index.pug'));
const getGallery = get('/gallery/:id', async ctx => {
  const images = await db.find({ id: ctx.params.id }).exec();
  return render('gallery.pug', { images });
});

Let's put it all together to see how they work:

const server = require('server');
const { get, post } = server.router;

const getHome = get('/', () => render('index.pug'));
const getGallery = get('/gallery/:id', async ctx => {
  const images = await db.find({ id: ctx.params.id }).exec();
  return render('gallery.pug', { images });
});

server(getHome, getGallery);

We can also receive post, del, error, socket and other request types through the router. To see them all, visit the Router documentation:

Router Documentation

Advanced topics

There is a lot of basic to mid-difficulty documentation to do until we even get here. Just a quick note so far:

The main function returns a promise that will be fulfilled when the server is running and can be accessed. It will receive a more primitive context. So this is perfectly valid:

server(ctx => 'Hello world').then(app => {
  console.log(`Server launched on http://localhost:${app.options.port}/`);
});

If you need to stop the server manually, you can do so by invoking the .close() function:

server(ctx => 'Hello world').then(async app => {
  console.log('Launched');
  await app.close();
  console.log('Closed');
});

This project is maintained by Francisco Presencia and is part of Francisco IO LTD (UK). It is a lot of work and I'd love if you or your company could help me keep building it and in the process I'll help you with Node.js.

All sponsors will receive an ebook and/or a book for free when released. Besides this, there are some sponsors tiers:

sponsorship perk credit (homepage + github)
$1,000+ email support logo (normal) + link + β™₯
$2,000+ live coding help of 10h logo (normal) + link + β™₯
$10,000+ in-person workshop of 20h logo (large) + link + β™₯

Get in touch

Notes and conditions

Donations are 0-99.99$ and sponsorships are 100$+.

All of the perks have a valid period of 1 year, later on they'd have to be renewed. The book/ebook has no duration and has to happen once.

I reserve the right to reject anything if I don't find it suitable, including but not limited to malign requests.

Everything in this page is negotiable and will be specified when getting in touch:

Thank you a lot for helping me improve server.js!

Get in touch Paypal me

Context

Context is the only parameter that middleware receives and contains all the information available at this point of the request:

name example type
.options { port: 3000, public: 'public' } Object
.data { firstName: 'Francisco '} Object
.params { id: 42 } Object
.query { search: '42' } Object
.session { user: { firstName: 'Francisco' } } Object
.headers { 'Content-Type': 'application/json' } Object
.cookies { acceptCookieLaw: true } Object
.files { profilepic: { ... } } Object
.ip '192.168.1.1' String
.url '/cats/?type=cute' String
.method 'GET' String
.path '/cats/' String
.secure true Boolean
.xhr false Boolean

It can appear at several points, but the most important one is as a middleware parameter:

// Load the server from the dependencies
const server = require('server');

// Display "Hello δΈ–η•Œ" for any request
const middleware = ctx => {
  // ... (ctx is available here)
  return 'Hello δΈ–η•Œ';
};

// Launch the server with a single middleware
server(middleware);

.options

An object containing all of the parsed options used by server.js. It combines environment variables and explicit options from server({ a: 'b' });:

const mid = ctx => {
  expect(ctx.options.port).toBe(3012);
};

/* test */
const res = await run({ port: 3012 }, mid, () => 200).get('/');
expect(res.status).toBe(200);

If we have a variable set in the .env or through some other environment variables, it'll use that instead as environment options take preference:

# .env
PORT=80
const mid = ctx => {
  expect(ctx.options.port).toBe(7693);
};

/* test */
const res = await run({ port: 7693 }, mid, () => 200).get('/');
expect(res.status).toBe(200);

.data

This is aliased as body as in other libraries. It is the data sent with the request. It can be part of a POST or PUT request, but it can also be set by others such as websockets:

const middle = ctx => {
  expect(ctx.data).toBe('Hello δΈ–η•Œ');
};

// Test it (csrf set to false for testing purposes)
run(noCsrf, middle).post('/', { body: 'Hello δΈ–η•Œ' });
run(middle).emit('message', 'Hello δΈ–η•Œ');

To handle forms sent normally:

//- index.pug
form(method="POST" action="/contact")
  input(name="email")
  input(name="_csrf" value=csrf type="hidden")
  input(type="submit" value="Subscribe")

Then to parse the data from the back-end:

const server = require('server');
const { get, post } = server.router;
const { render, redirect } = server.reply;

server([
  get(ctx => render('index.pug')),
  post(ctx => {
    console.log(ctx.data);  // Logs the email
    return redirect('/');
  })
]);

.params

Parameters from the URL as specified in the route:

const mid = get('/:type/:id', ctx => {
  expect(ctx.params.type).toBe('dog');
  expect(ctx.params.id).toBe('42');
});

// Test it
run(mid).get('/dog/42');

They come from parsing the ctx.path with the package path-to-regexp. Go there to see more information about it.

const mid = del('/user/:id', ctx => {
  console.log('Delete user:', ctx.params.id);
});

.query

The parameters from the query when making a request. These come from the url fragment ?answer=42&...:

const mid = ctx => {
  expect(ctx.query.answer).toBe('42');
  expect(ctx.query.name).toBe('Francisco');
};

// Test it
run(mid).get('/question?answer=42&name=Francisco');

.session

After following the sessions in production tutorial, sessions should be ready to get rolling. This is an object that persist among the user refreshing the page and navigation:

// Count how many pages the visitor sees
const mid = ctx => {
  ctx.session.counter = (ctx.session.counter || 0) + 1;
  return ctx.session.counter;
};

// Test that it works
run(ctx).alive(async ctx => {
  await api.get('/');
  await api.get('/');
  const res = await api.get('/');
  expect(res.body).toBe('3');
});

.headers

Get the headers that were sent with the request:

const mid = ctx => {
  expect(ctx.headers.answer).toBe(42);
};

// Test it
run(mid).get('/', { headers: { answer: 42 } });

.cookies

Object that holds the cookies sent by the client:

const mid = ctx => {
  console.log(ctx.cookies);
};

run(mid).get('/');

.files

Contains any and all of the files sent by a request. It would normally be sent through a form with an <input type="file"> field or through a FormData in front-end javascript:

<form method="POST" action="/profilepic" enctype="multipart/form-data">
  <input name="profilepic" type="input">
  <input type="hidden" name="_csrf" value="{{_csrf}}">
  <input type="submit" value="Send picture">
</form>

Note the csrf token and the enctype="multipart/form-data", both of them needed. Then to handle it with Node.js:

const mid = post('/profilepic', ctx => {
  // This comes from the "name" in the input field
  console.log(ctx.files.profilepic);
  return redirect('/profile');
});

.ip

The IP of the client. If your server is running behind a proxy, it uses the de-facto standard x-forwarded-for header to get the right client IP:

const mid = ctx => {
  console.log(ctx.ip);
};

run(mid).get('/');

It can be useful with services like geoip-lite to find the user's location:

// Localize user depending on their IP
const geoip = require('geoip-lite');

module.exports = ctx => {
  ctx.geo = geoip.lookup(ctx.ip);
};

// {
//   range: [ 3531655168, 3531657215 ],
//   country: 'JP',
//   region: '24',
//   eu: '0',
//   timezone: 'Asia/Tokyo',
//   city: 'Yokkaichi',
//   ll: [ 34.9667, 136.6167 ],
//   metro: 0,
//   area: 50
// }

.url

The full cuantified URL:

const mid = ctx => {
  expect(ctx.url).toBe('/hello?answer=42');
};

run(mid).get('/hello?answer=42');

.method

The request method, it can be GET, POST, PUT, DELETE:

const mid = ctx => {
  expect(ctx.method).toBe('GET');
};

// Test it
run(mid).get('/');

Or other methods:

const mid = ctx => {
  expect(ctx.method).toBe('POST');
};

// Test it
run(noCsrf, mid).post('/');

.path

Only the path part from the URL. It is the full URL except for the query:

const mid = ctx => {
  expect(ctx.path).toBe('/question');
};

// Test it
run(mid).get('/question?answer=42');

.secure

Returns true if the request is made through HTTPS. Take into account that if you are behind Cloudflare or similar it might be reported as false even though your clients see https:

const mid = ctx => {
  expect(ctx.secure).toBe(false);
};

// Test it
run(mid).get('/');

.xhr

A boolean set to true if the request was done through AJAX. Specifically, if X-Requested-With is β€œXMLHttpRequest”:

const mid = ctx => {
  expect(mid.xhr).toBe(false);
};

run(mid).get('/');

Errors

If you happen to stumble here, this bit of the documentation is outdated and follows some old code. Please help us improve the project and the docs so we can make it into the official release.

There are many type of errors that can occur with server.js and here we try to explain them and how to fix them. They are divided by category: where/why they are originated.

We also overview here how to handle errors. You have to first define it, then throw the error and finally handle the error.

Define an error

To define an error in your code the best way to do it is to use the package human-error (by the author of server), since it's made to combine perfectly with server.js. In the future we might integrate it, but so far they are kept separated.

To define an error, create a different file that will contain all or part of your errors, here called errors.js for our site mycat.com:

// errors.js
const errors = require('human-error')();  // <-- notice this

errors['/mycat/nogithubsecret'] = `
  There is no github secret set up. Make sure you have saved it in your '.env',
  and if you don't have access go see Tom and he'll explain what to do next.
  https://mycat.com/guide/setup/#github
`;

module.exports = errors;

Throw the error

Now let's use it, to do so we'll just need to import this file and throw the corresponding error:

const server = require('server');
const HumanError = require('./errors');

server(ctx => {
  if (!ctx.options.githubsecret) {
    throw new HumanError('/mycat/nogithubsecret');
  }
});

Try it! Run the code with node . and try accessing http://localhost:3000/. You should see a server error on the front-end and the proper description in the back-end.

Error handling

Now this was an error for the developers where we want to be explicit and show the error clearly. For users thought things change a bit and are greatly improved by server's error handling.

First let's deal with super type checking:

const route = get('/post/:id', ctx => {
  if (!/^\d+$/.test(ctx.params.id)) {
    throw new HumanError('/mycat/type/invalid', { base: '/post' });
  }
});

// Handle a wrong id error and redirect to a 404
const handle = error('/mycat/type/invalid', async ctx => {
  return redirect(`/${ctx.error.base || ''}?message=notfound`);
});

// Handle all type errors in the namespace "mycat"
const handleType = error('/mycat/type', () => {
  return redirect(`/${ctx.error.base || ''}?message=notfound`);
});

// Handle all kind of unhandled errors in the namespace "mycat"
const handleAll = error('/mycat', () => {
  return status(500);
});

Let's say that someone is trying to access something they don't have access to. Like deleting a comment that is not theirs:

// comments.js
module.exports = [
  ...
  del('/comment/:id', async ctx => {
    const comment = await db.comment.findOne({ _id: ctx.params.id });
    if (!comment.author.equals(ctx.user._id)) {
      throw new HumanError('/mycat/auth/unauthorized', { user: ctx.user._id });
    }
  })
];

Later on you can handle this specific error, we could log these specific kind of errors, etc.

Native

/server/native/portused

This happens when you try to launch server in a port that is already being used by another process. It can be another server process or a totally independent process. To fix it you can do:

  • Check that there are no other terminals running this process already.
  • Change the port for the server such as server({ port: 5000 });.
  • Find out what process is already using the port and stop it. In Linux: fuser -k -n tcp 3000.

Example on when this error is happening:

const server = require('server');
// DO NOT DO THIS:
server(3000);
server(3000);

To fix it, invoke it with a different port:

const server = require('server');
server(2000);
server(3000);

Options

These errors are related to server's options.

/server/options/portnotanumber

Core

These errors occur when handling a specific part of server.js.

/server/core/missingmiddleware

This will normally happen if you are trying to create a server middleware from an express middleware but forget to actually pass express' middleware.

This error happens when you call modern() with an empty or falsy value:

const { modern } = server.utils;
const middle = modern();  // Error

/server/core/invalidmiddleware

This happens when you try to call modern() with an argument that is not an old-style middleware. The first and only argument for modern() is a function with express' middleware signature.

This error should also tell you dynamically which type of argument you passed.

const { modern } = server.utils;
const middle = modern('hello');

Options

Available options, their defaults, types and names in .env:

name default .env type
port 3000 PORT=3000 Number
secret 'secret-XXXX' SECRET=secret-XXXX String
public 'public' PUBLIC=public String
views 'views' VIEWS=views String
engine 'pug' ENGINE=pug String
env 'development' NODE_ENV=development String
favicon false FAVICON=public/logo.png String
parse [info] [info] Object
session [info] [info] Object
socket [info] [info] Object
security [info] [info] Object
log 'info' LOG=info String

You can set those through the first argument in server() function:

// Import the main library
const server = require('server');

// Launch the server with the options
server({
  port: 3000,
  public: 'public',
});

The options preference order is this, from more important to less:

  1. .env: the variable within the environment.
  2. server({ OPTION: 3000 }): the variable set as a parameter when launching the server.
  3. defaults: defaults will be used as can be seen below

They are accessible for your dev needs through ctx.options (read more in context options):

server(ctx => console.log(ctx.options));
// { port: 3000, public: './public', ... }

Environment

Environment variables are not commited in your version control but instead they are provided by the machine or Node.js process. In this way these options can be different in your machine and in testing, production or other type of servers.

They are uppercase and they can be set through a file called literally .env in your root folder:

PORT=3000
PUBLIC=public
SECRET=secret-XXXX
ENGINE=pug
NODE_ENV=development

Remember to add .env to your .gitignore.

To set them in remote server it will depend on the hosting that you use (see Heroku example).

Argument

The alternative to the environment variables is to pass them as the first argument when calling server(). Each option is a combination of key/value in the object and they all go in lowercase. See some options with their defaults:

const server = require('server');

server({
  port: 3000,
  public: 'public',
  secret: 'secret-XXXX',
  engine: 'pug',
  env: 'development'   // Remember this is "env" and not "node_env" here
});

Special cases

As a general rule, an option that is an object becomes a _ separated string in uppercase for the .env file. For example, for the SSL we have to pass an object such as:

server({
  port: 3000,
  ssl: {
    key: './ssl.pem',
    cert: './ssl.cert'
  }
});

So if we want to put this in the environment variable we'd set it up such as:

PORT=3000
SSL_KEY=test/fixtures/keys/agent2-key.pem
SSL_CERT=test/fixtures/keys/agent2-cert.cert

The converse is not true; a _ separated string in the .env does not necessarily become an object as a parameter. You'll have to read the documentation of each option and plugin for the specific details.

Port

The port where you want to launch the server. Defaults to process.env.PORT or 3000 if not found, and it's the only option that can be specified as a single option:

server();        // Use the default port 3000
server(3000);    // Specify the port
server({ port: 3000 });  // The same as the previous one

If you are setting the port in your environment that will take preference over the argument as all environment variables. So it will work seamlessly in Heroku and other hosts that define a PORT environment variable.

Or you can leave it empty and just use your .env file:

PORT=3000

Example: setting the port to some other number. For numbers 1-1024 you'd need administrator permission, so we're testing it with higher ports:

const options = {
  port: 5001
};

/* test */
const same = ctx => ({ port: ctx.options.port });
const res = await run(options, same).get('/');
expect(res.body.port).toBe(5001);

Secret

It is highly recommended that you set this in your environment variable for both development and production before you start coding. It should be a random and long string. It can be used by middleware for storing secrets and keeping cookies/sessions:

SECRET=your-random-string-here

The default provided will be different each time the server is launched. This is not suitable for production, since you want persistent sessions even with server restarts. See the session in production tutorial to set it up properly (includes some extras such as Redis sessions).

It cannot be set as a variable: server({ secret: 'whatever' });.

If you have other secrets it is recommended that you prepend those by their respective vendor names:

# Used by server.js for sessions
SECRET=your-random-string-here

# Used by different middleware
GITHUB_SECRET=your-github-secret
CLOUDINARY_SECRET=your-cloudinary-secret
# ...

Public

name default .env type notes
public public PUBLIC=public String Folder path

The folder where your static assets are. This includes images, styles, javascript for the browser, etc. Any file that you want directly accessible through the browser such as example.com/myfile.pdf should be in this folder. You can set it to any folder within your project.

To set the public folder in the environment add this to your .env:

PUBLIC=public

Through the initialization parameter:

const options = {
  public: 'public'
};

/* test */
const same = ctx => ({ public: ctx.options.public });
const res = await run(options, same).get('/');
expect(res.body.public).toBe(path.join(process.cwd() + '/public'));

To set the root folder specify it as './':

const options = {
  public: './'
};

/* test */
const same = ctx => ({ public: ctx.options.public });
const res = await run(options, same).get('/');
expect(res.body.public).toBe(process.cwd() + path.sep);

If you don't want any of your files to be accessible publicly, then you can cancel it through a false or empty value:

server({ public: false });
server({ public: '' });

Views

name default .env type notes
views views VIEWS=views String Folder path

The folder where you put your view files, partials and templates. These are the files used by the render() method. You can set it to any folder within your project.

It walks the given directory so please make sure not to include the e.g. root directory since then it'll attempt to walk node_modules and that might delay the time to to launch the server significantly.

To set the views folder in the environment add this to your .env:

VIEWS=views

Or pass it as another option:

const options = {
  views: 'views'
};

/* test */
const same = ctx => ({ views: ctx.options.views });
const res = await run(options, same).get('/');
expect(res.body.views).toBe(path.join(process.cwd(), 'views') + path.sep);

You can set it to any folder, like ./templates:

const options = {
  views: './templates'
};

/* test */
options.views = './test/views';
const same = ctx => ({ views: ctx.options.views });
const res = await run(options, same).get('/');
expect(res.body.views).toBe(process.cwd() + path.sep + 'test/views' + path.sep);

If you don't have any view file you don't have to create the folder. The files within views should all have an extension such as .hbs, .pug, etc. To see how to install and use those keep reading.

Engine

name default .env type notes
engine engine ENGINE=engine String, Object engine

Note: this option, as all options, can be ignored and server.js will work with both .pug and .hbs (Handlebars) file types.

The view engine that you want to use to render your templates. See all the available engines. To use an engine you normally have to install it first except for the pre-installed ones pug and handlebars:

npm install [ejs|nunjucks|emblem] --save

Then to use that engine you just have to add the extension to the render() method:

// No need to specify the engine if you are using the extension
server(ctx => render('index.pug'));
server(ctx => render('index.hbs'));
// ...

However if you want to use it without extension, you can do so by specifying the engine in .env:

ENGINE=pug

Or through the corresponding option in javascript:

server({ engine: 'pug' }, ctx => render('index'));

The files will be relative to your views folder. When using hbs, the views folder will also be used to load your partials, so you can write them like this:

<!-- `views/index.hbs` -->
<html>
  <!-- This file is in `views/head.hbs`. Note how we also pass a variable -->
  {{> head title="Hello world" }}
  <body>
    <!-- This file is in `views/partials/nav.hbs` -->
    {{> partials/nav }}
    ...
  </body>
</html>

Writing your own engine

Engines are really easy to write with server.js. They must be a function that receives the file path and the options (or locals) and returns the text to render from the engine. It can be either sync or async. To configure it for handling a specific extension, just put that as the key in an object for engine.

As an example of how to handle nunjucks, in a single file for it:

// nunjucks-engine.js
const nunjucks = require('nunjucks');

// The .render() in Nunjucks is sync, so no need to wait
module.exports = (file, options) => nunjucks.render(file, options);

Then in your main file:

const server = require('server');
const { get } = server.router;
const { render } = server.reply;
const nunjucks = require('./nunjucks');

const options = {
  engine: {
    // Register two keys for the same render function
    nunjucks,
    njk: nunjucks
  }
};

server(options, [
  get('/', () => render('index.njk', { a: 'b' })),
  get('/hello', () => render('hello.nunjucks'))
]);

You can also set the function to async and it will wait until it is resolved, and return the result to the browser as expected.

Env

name default .env type notes
env development NODE_ENV=development String, Object ['development', 'test', 'production']

Define the context in which the server is running. It has to be one of these: 'development', 'test' or 'production'. Some functionality might vary depending on the environment, such as live/hot reloading, cache, etc. so it is recommended that you set these appropriately.

Note: The environment variable is called NODE_ENV while the option as a parameter is env.

This variable does not make sense as a parameter to the main function, so we'll normally use this within our .env file. See it here with the default development:

NODE_ENV=development

Then in your hosting environment you'd set it to production (some hosts like Heroku do so automatically):

NODE_ENV=production

These are the only accepted types for NODE_ENV:

development
test
production

You can check those within your code like:

server(ctx => {
  console.log(ctx.options.env);
});

Favicon

To include a favicon, specify its path with the favicon key:

const server = require('server');

server({ favicon: 'public/favicon.png' },
  ctx => 'Hello world'
);

The path can be absolute or relative to the root of your project.

Most browsers require /favicon.ico automatically, so you might be seeing 404 errors if the favicon is not returned for this situation.

Parse

The parsing middleware is included by default. It uses few of them under the hood and these are the options for all of them. They should all work by default, but still give access to the options if you want to make some more advanced modifications.

Body parser

This is the name for the default parser for <form> without anything else. The technical name and for those coming from express is urlencoded. See the available options in the middleware documentation.

As an example, let's say that you want to upgrade from the default limit of 100kb to 1mb:

server({
  parser: {
    body: { limit: '1mb' }
  }
});

JSON parser

This will parse JSON requests into the actual variables. See the available options in this middleware documentation.

As an example, let's say (as above) that we want to change the limit for requests from 100kb to 1mb. To do so, change the json parser option:

server({
  parser: {
    json: { limit: '1mb' }
  }
});

You can also combine the two above:

server({
  parser: {
    body: { limit: '1mb' },
    json: { limit: '1mb' }
  }
});

Text parser

Plain ol' text. As with the other examples, refer to the middleware full documentation for more comprehensive docs.

An example, setting the size limit for the requests:

server({
  parser: {
    text: { limit: '1mb' }
  }
});

Data parser

This is for file uploads of any type. It uses Formidable underneath, so refer to the Formidable documentation for the full list of options.

An example:

server({
  parser: {
    data: { uploadDir: '/my/dir' }
  }
});

Cookie parser

For using cookies, it uses cookie-parser underneath so refer to express documentation for the full list of options.

An example:

server({
  parser: {
    cookie: {
      maxAge: 900000,
      httpOnly: true
    }
  }
});

Session

It accepts these options as an object:

server({ session: {
  resave: false,
  saveUninitialized: true,
  cookie: {},
  secret: 'INHERITED',
  store: undefined,
  redis: undefined
}});

You can read more about these options in Express' package documentation.

All of them are optional. Secret will inherit the secret from the global secret if it is not explicitly set.

If the session.redis option or the env REDIS_URL is set with a Redis URL, a Redis store will be launched to achieve persistence in your sessions. Read more about this in the tutorial Sessions in production. Example:

# .env
REDIS_URL=redis://:password@hostname:port/db_number
// index.js
const server = require('server');

// It will work by default since it's an env variable
server({}, ...);

Otherwise, to pass it manually (not recommended) pass it through the options:

const redis = 'redis://:password@hostname:port/db_number';
server({ session: { redis } }, ...);

Session Stores

To use one of the many available third party session stores, pass it as the store parameter:

// Create your whole store thing
const store = ...;

// Use it within the session
server({ session: { store } }, ...);

Many of the stores will need you to pass the raw session initially like this:

const RedisStore = require('connect-redis')(session);
const store = RedisStore({ ... });

You can access this variable through server.session after requiring server:

const server = require('server');
const RedisStore = require('connect-redis')(server.session);
const store = RedisStore({ ... });

server({ session: { store } }, ...);

Socket

You can pass here the options for socket.io:

server({
  socket: {
    path: '/custompath'
  }
});

This is the equivalent of doing this with socket.io:

const io = socket(server, { path: '/custompath' });

You can see an example on how it's used in the websocket example.

Security

It combines Csurf and Helmet to give extra security:

server({
  security: {
    csrf: {
      ignoreMethods: ['GET', 'HEAD', 'OPTIONS'],
      value: req => req.body.csnowflakerf
    },
    frameguard: {
      action: 'deny'
    }
  }
});

We are using Helmet for great security defaults. To pass any helmet option, just pass it as another option in security:

server({
  security: {
    frameguard: {
      action: 'deny'
    }
  }
});

For quick tests/prototypes, the whole security plugin can be disabled (not recommended):

server({ security: false });

Individual parts can also be disabled like this. This makes sense if you use other mechanisms to avoid CSRF, such as JWT:

server({
  security: {
    csrf: false
  }
});

Their names in the .env are those:

SECURITY_CSRF
SECURITY_CONTENTSECURITYPOLICY
SECURITY_EXPECTCT
SECURITY_DNSPREFETCHCONTROL
SECURITY_FRAMEGUARD
SECURITY_HIDEPOWEREDBY
SECURITY_HPKP
SECURITY_HSTS
SECURITY_IENOOPEN
SECURITY_NOCACHE
SECURITY_NOSNIFF
SECURITY_REFERRERPOLICY
SECURITY_XSSFILTER

Log

Display some data that might be of value for the developers. This includes from just some information up to really important bugs and errors notifications.

You can set several log levels and it defaults to 'info':

  • emergency: system is unusable
  • alert: action must be taken immediately
  • critical: the system is in critical condition
  • error: error condition
  • warning: warning condition
  • notice: a normal but significant condition
  • info: a purely informational message
  • debug: messages to debug an application

Do it either in your .env:

LOG=info

Or as a parameter to the main function:

server({ log: 'info' });

To use it do it like this:

server(ctx => {
  ctx.log.info('Simple info message');
  ctx.log.error('Shown on the console');
});

If we want to modify the level and only show the warnings or more important logs:

server({ log: 'warning' }, ctx => {
  ctx.log.info('Not shown anymore');
  ctx.log.error('Shown on the console');
});

Advanced logging

You can also pass a report variable, in which case the level should be specify as level:

server({
  log: {
    level: 'info',
    report: (content, type) => {
      console.log(content);
    }
  }
});

This allows you for instance to handle some specific errors in a different way. It is also useful for testing that the correct data is printed on the console in certain situations.

Plugins

If you happen to stumble here, this bit of the documentation is under active construction and should not be used at all. Please help us improve the project and the docs.

Create a plugin

API

Here comes the big one, plugins. First, a wish list. I'd like for a plugin to have this API available:

module.exports = {

  // This is working right now (highly unstable)
  // String
  name: 'whatever',

  // Object
  config: {}

  // Function, Array
  init: () => {},

  // Function, Array
  before: () => {},

  // Function, Array
  after: () => {},

  // Function, Array
  final: () => {},


  // Not working yet but desirable:
  // Function (named 'whatever' like the plugin), Object with { name: fn } pairs
  router: () => {},

  // Function (named 'whatever' like the plugin), Object with { name: fn } pairs
  reply: () => {}
};

Now, I am not 100% it makes sense to open router and reply right now. I think there are some situations where it'd be really useful, like sending a PDF back for example:

// Send a pdf from server
server(ctx => pdf('./readme.pdf'));

But there are many options here and doing it one way might limit some other options. So my first question:

Simple example: database

Why are these useful? Isn't is enough with middleware?

Well no, for instance for database connections it is really useful. Let's say we develop a @server/mongoose and install it with npm install @server/mongoose. Afterwards, just passing the options and we have access to the db through the context:

server({ mongoose: 'url for mongodb (or in .env)' },
  get('/', ctx => 'Hello world'),
  get('/sales', hasUser, ctx => ctx.db.sales.find({ user: ctx.user.id }))
});

This can be applied to anything that has to be connected or configured once initially and later on can be used in the middleware.

Advanced example: sass

It also opens up to new possibilities, let's see a small example with sass. Let's say that we want to make a sass plugin that rebuilds the whole thing on each request for dev and only once on production:

module.exports = {
  name: 'sass',
  options: {
    __root: 'source',
    source: {
      default: 'style/style.scss',
      type: String,
      file: true
    },
    destination: {
      default: 'public/style.css',
      type: String
    }
  },
  init: async ctx => {

    // If `ctx.options.sass.destination` exists and was not generated by @server/sass
    //   throw an early error and ask for the file to be (re)moved

    // Remove the `ctx.options.sass.destination` file

    if (ctx.options.env === 'production') {
      // Compile everything and store it in `ctx.options.sass.destination`
    }
  },

  // This will only get called in dev+test, since `style.css` will be found in production
  before: get('/style.css', async ctx => {
    // Reply with the whole `ctx.options.sass.source` compiled dynamically
  })
};

To use it is really simple. First npm install @server/sass, then if your options are the default ones you won't even need to write any specific javascript for it. Let's say though that we want to change our source file, which is the root option:

server({ sass: './front/style.sass' }, ctx => render('index'));

That's it, with the flexibility of plugins you wouldn't need any more code to have a sass plugin.

This is why I think plugins can be really awesome if they are built and documented properly. What plugin would you like to see?

Options

A small exploration about how the options for plugins might look like for a developer of a plugin. Now I've written quite a few and have a better idea of the possibilities and limitations of them. I will be using log as an example. For the simple way with defaults:

plugin.options = {
  level: { default: 'info' },
  reporter: { default: process.stdout },
  __root: 'level'
};

There are no mandatory fields, however setting a default is strongly recommended. The last root bit would make both of these usages equivalent when using the plugin log:

server({
  log: 'info'
});

server({
  log: { level: 'info' }
});

The .env is also quite straightforward in this situation thanks to the __root option. Both of these are equivalent as well:

# Single option
LOG=info

# Multiple options (note: cannot do a function here though!)
LOG_LEVEL=info

Now you might want all bells and whistles going on. For instance, let's define the type of the parameter. This will add a small validate function internally:

log.options = {
  level: {
    default: 'info',
    type: String
  }
};

List of advanced options with their defaults inspired by Mongoose. First, options to use the correct variable:

  • default: the default to set in case it is not set. Leave it unset and it won't have a value if it is not explicitly set.
  • env: NAME || true: defines the name for that variable in the environment variables. If set to false, it will not accept it through the environment.
  • arg: NAME || true: defines the key of the value for options in server(OPTIONS). If set to false it will not accept it from the options object (some arguments that MUST only be accepted through the environment).
  • inherit: NAME || false: passing a name, it inherits the value from a global variable by this order of preference: [validate:] specific environment > global environment > specific argument > global argument > specific default > global default.
  • find: FN || false: a function that receives the options passed on the main function, then all of the environment and finally the default. It returns the wanted value.
  • extend: true || false: extend the default value for the unwritten properties with the default props if they are not set. Value passed: { main: 'a', second: 'b' }, { default: { second: 'c', third: 'd' }, extend: true } => { main: 'a', second: 'b', third: 'd' }.

Then you can perform several validations:

  • required: FN || false: make sure the option is set before proceeding. This doesn't make sense when default is set.
  • type: false: define the type of the variable. A type or an array of types. Will only check the primitive types Boolean, Number, String, Array, Object. Can be also an array of types.
  • enum: ['a', 'b']: the variable should be within the list.
  • validate: FN || false: defines a function to validate the value. It will accept first the current value, then all the currently set values and must return true for a valid value or false otherwise. Note: this is done AFTER any of the other specific checks like type check or the enumerate check.

Note: all of the functions described here can be either synchronous or asynchronous by returning a Promise (or using the async keyword).

TODO: check the engine for mongoose to see if it makes sense to extract the validation part.

Routes for Plugins

I have long been wondering whether the routes and reply should be extensible. There are advantages and disadvantages to this.

On one hand, we can stick to more traditional requests workflow: get, post, put, delete and socket seem like a really good scope.

But then, if you think about plugins and the possibilities there is so much more we can do and make a nice abstraction layer.

For example, let's say you are making a LINE bot and there is a LINE plugin for server. This plugin can behave as normal:

server(
  ..
  post('/line', ctx => {
    ctx.line.sendMessage('Hello world');
  })
);

This is the traditional way. But if we don't limit ourselves to that, we could be doing it one abstraction level up where the implementation details are invisible:

const { line } = server.router;
const { message } = server.reply;

server(
  line(ctx => {
    return message('Hello world');
  })
);

One of the big issues here would be namespacing. You might want to have a line router but also a line reply, and the same with some of the others I can think right now: sms, email, etc.

In the example above, you might be tempted to name your reply message(), but so might other plugin.

Possible solutions: for the plugins you don't get the router from the main server router, but you get it from the plugin:

const line = require('@server/line');

server(
  line.router(ctx => {
    return line.reply.message();
  })
);

This feels too verbose for the otherwise succint server sintax, but right now seems like the best solution.

Another solution would be namespacing the whole server in general. This would also ease one of the pain points I am finding more and more frequently: having to import every route/reply I want to use manually:

const server = require('server');
const { router, reply } = server;

server(
  router.get('/', ctx => reply.render(...)),
  router.post('/', ctx => reply.json(...))
);

While it is a bit more verbose, it cuts down on the import logic (favoring smaller files), makes things more explicit and clear and it's really compatible with this idea of plugins:

const server = require('server');
const { router, reply } = server;

server(
  router.get('/', ctx => reply.render(...)),
  router.post('/', ctx => reply.json(...)),
  router.line(ctx => reply.line(...)),
  // OR
  router.line(ctx => reply.line.message(...))
);

Reply

A reply is a method returned from a middleware that creates the response. These are the available methods and their parameters for server.reply:

reply name example final
cookie(name, value, opts) cookie('name', 'Francisco') false
download(path[, filename]) download('resume.pdf') true
file(path) file('resume.pdf') true
header(field[, value]) header('ETag': '12345') false
json([data]) json({ hello: 'world' }) true
jsonp([data]) jsonp({ hello: 'world' }) true
redirect([status,] path) redirect(302, '/') true
render(view[, locals]) render('index.hbs') true
send([body]) send('Hello there') true
status(code) status(200) mixed
type(type) type('html') false

Examples:

const { get, post } = require('server/router');
const { render, redirect, file } = require('server/reply');

module.exports = [
  get('/', ctx => render('index.hbs')),
  post('/', processRequest, ctx => redirect('/'))
];
Make sure to **return** the reply that you want to use. It won't work otherwise.

The ctx argument is explained in middleware's Context. The reply methods can be imported in several ways:

// For whenever you have previously defined `server`
const { send, json } = server.reply;

// For standalone files:
const { send, json } = require('server/reply');

There are many more ways of importing the reply methods, but those above are the recommended ones.

Chainable

While most of the replies are final and they should be invoked only once, there are a handful of others that can be chained. These add something to the ongoing response:

You can chain those among themselves and any of those with a final method that sends. If no final method is called in any place the request will be finished with a 404 response.

The status() reply can be used as final or as chainable if something else is added.

Return value

Both in synchronous mode or asyncrhonous mode you can just return a string to create a response:

// Send a string
const middle = ctx => 'Hello δΈ–η•Œ';

// Test it
const res = await run(middle).get('/');
expect(res.body).toBe('Hello δΈ–η•Œ');

Returning an array or an object will stringify them as JSON:

server(ctx => ['life', 42]);
// Note: extra parenthesis needed by the arrow function to return an object
server(ctx => ({ life: 42 }));

A single number will be interpreted as a status code and the corresponding body for that status will be returned:

server(get('/nonexisting', => 404));

You can also throw anything to trigger an error:

const middle = ({ req }) => {
  if (!req.body) {
    throw new Error('No body provided');
  }
}

const handler = error(ctx => ctx.error.message);

// Test it
const res = await run(middle, handler).get('/nonexisting');
expect(res.body).toBe('No body provided');

Multiple replies

Another important thing is that the first reply used is the one that will be used. However, you should try to avoid this and we might make it more strict in the future:

// I hope you speak Spanish
server([
  ctx => 'Hola mundo',
  ctx => 'Hello world',
  ctx => 'γ“γ‚“γ«γ‘γ―γ€δΈ–η•Œ'
]);

To avoid this, just specify the url for each request in a router:

// I hope you speak Spanish
server([
  get('/es', ctx => 'Hola mundo'),
  get('/en', ctx => 'Hello world'),
  get('/jp', ctx => 'γ“γ‚“γ«γ‘γ―γ€δΈ–η•Œ')
]);

Then each of those URLs will use a different language.

Send one or multiple cookies to the browser:

cookie(name, value, [options])

By default it will just set a cookie and not finish the request, so if you want to also send the body you need to do it explicitly:

server(
  ctx => cookie('foo', 'bar'),  // Set a cookie
  ctx => cookie('xyz', { obj: 'okay' }), // Stringify the object
  ctx => cookie('abc', 'def', { maxAge: 100000 }),  // Pass some options
  ctx => cookie('fizz', 'buzz').send(), // Set cookie AND finish the request
);
const { cookie } = server.reply;
const setCookie = ctx => cookie('foo', 'bar').send();

// Test
run(setCookie).get('/').then(res => {
  expect(res.headers['Set-Cookie:']).toMatch(/foo\=bar/);
});

Cookie Options

The options is an optional object with these keys/values:

Key Default Type
domain Current domain String
encode encodeURIComponent Function
expires undefined (session) Date
httpOnly false Boolean
maxAge undefined (session) Number
path "/" String
secure false Boolean
signed false Boolean
sameSite false Boolean or String

See a better explanation of each one of those in express' documentation.

download()

An async function that takes a local path and an optional filename. It will return the local file with the filename name for the browser to download.

server(ctx => download('user-file-5674354.pdf'));
server(ctx => download('user-file-5674354.pdf', 'report.pdf'));

You can handle errors for this method downstream:

server([
  ctx => download('user-file-5674354.pdf'),
  error(ctx => { console.log(ctx.error); })
]);

file()

Send a file to the browser with the correct mime type:

server(ctx => file('user-profile-5674354.png'));

It does not accept a name since the user is not prompted for download. It will stream the file, so that no files are read fully into memory.

You can handle errors for this method downstream:

server([
  ctx => file('user-profile-5674354.png'),
  error(ctx => { console.log(ctx.error); })
]);

Set a header to be sent with the response. It accepts two strings as key and value or an object to set multiple headers:

const mid = ctx => header('Content-Type', 'text/plain');
const mid2 = ctx => header('Content-Length', '123');

// Same as above
const mid = ctx => header({
  'Content-Type': 'text/plain',
  'Content-Length': '123'
});

You can also send multiple headers with the same name by passing an array as the second parameter:

const mid = ctx => header({
  'Link': ['Fake Value', 'Another Fake Value']
});

This can be chained with other methods to e.g. prompt for a download:

const mid = async ctx => {
  const data = await readFileAsync('./hello.pdf', 'utf-8');
  return header({ 'Content-Disposition': `filename='welcome.pdf'` })
    .type('application/pdf')
    .send(new Buffer(data));
};

json()

Sends a JSON response. It accepts a plain object or an array that will be stringified with JSON.stringify. Sets the correct Content-Type headers as well:

const mid = ctx => json({ foo: 'bar' });

// Test it
run(mid).get('/').then(res => {
  expect(res.body).toEqual(`{"foo":"bar"}`);
});

jsonp()

Same as json() but wrapped with a callback. Read more about JSONP:

const mid = ctx => jsonp({ foo: 'bar' });

// Test it
run(mid).get('/?callback=callback').then(res => {
  expect(res.body).toMatch('callback({foo:"bar"})');
});

It is useful for loading data Cross-Domain. The query ?callback=foo is mandatory and you should set the callback name there:

const mid = ctx => jsonp({ foo: 'bar' });

// Test it
run(mid).get('/?callback=foo').then(res => {
  expect(res.body).toMatch('foo({foo:"bar"})');
});

redirect()

Redirects to the url specified. It can be either internal (just a path) or an external URL:

const mid1 = ctx => redirect('/foo');
const mid2 = ctx => redirect('../user');
const mid3 = ctx => redirect('https://google.com');
const mid4 = ctx => redirect(301, 'https://google.com');

render()

This is the most complex method and yet the most useful one. It takes a filename and some data and renders it:

const mid1 = ctx => render('index.hbs');
const mid2 = ctx => render('index.hbs', { user: 'Francisco' });

The filename is relative to the views option (defaults to 'views'):

// Renders PROJECT/somefolder/index.hbs
server({ views: 'somefolder' }, ctx => render('index.hbs'));

The extension of this filename is optional. It accepts by default .hbs, .pug and .html and can accept more types installing other engines:

const mid1 = ctx => render('index.pug');
const mid2 = ctx => render('index.hbs');
const mid3 = ctx => render('index.html');

The data will be passed to the template engine. Note that some plugins might pass additional data as well.

send()

Send the data to the front-end. It is the method used by default with the raw returns:

const mid1 = ctx => send('Hello δΈ–η•Œ');
const mid2 = ctx => 'Hello δΈ–η•Œ';

However it supports many more data types: String, object, Array or Buffer:

const mid1 = ctx => send('Hello δΈ–η•Œ');
const mid2 = ctx => send('<p>Hello δΈ–η•Œ</p>');
const mid4 = ctx => send({ foo: 'bar' });
const mid3 = ctx => send(new Buffer('whatever'));

It also has the advantage that it can be chained, unlike just returning the string:

const mid1 = ctx => status(201).send({ resource: 'foobar' });
const mid2 = ctx => status(404).send('Not found');
const mid3 = ctx => status(500).send({ error: 'our fault' });

status()

Sets the status of the response. If no reply is done, it will become final and send that response message as the body:

const mid1 = ctx => status(404);  // The same as:
const mid2 = ctx => status(404).send('Not found');

type()

Set the Content-Type header for the response. It can be a explicit MIME type like these:

const mid1 = ctx => type('text/html').send('<p>Hello</p>');
const mid2 = ctx => type('application/json').send(JSON.stringify({ foo: 'bar' }));
const mid3 = ctx => type('image/png').send(...);

Or you can also write their more friendly names for an equivalent result:

const mid1 = ctx => type('.html');
const mid2 = ctx => type('html');
const mid3 = ctx => type('json');
const mid4 = ctx => type('application/json');
const mid5 = ctx => type('png');

Router

Available methods and their parameters for server.router:

route name example
get(PATH, FN1, FN2, ...) get('/', ctx => { ... })
head(PATH, FN1, FN2, ...) head('/', ctx => { ... })
post(PATH, FN1, FN2, ...) post('/', ctx => { ... })
put(PATH, FN1, FN2, ...) put('/', ctx => { ... })
del(PATH, FN1, FN2, ...) del('/', ctx => { ... })
error(NAME, FN1, FN2, ...) error('user', ctx => { ... })
sub(SUBDOMAIN, FN1, FN2, ...) sub('es', ctx => { ... })
socket(NAME, FN1, FN2, ...) socket('/', ctx => { ... })

A router is a function that tells the server how to handle each request. They are a specific kind of middleware that wraps your logic and acts as a gateway:

// Import methods 'get' and 'post' from the router
const { get, post } = require('server/router');

server([
  get('/', ctx => { /* ... */ }),      // Render homepage
  get('/users', ctx => { /* ... */ }), // GET requests to /users
  post('/users', ctx => { /* ... */ }) // POST requests to /users
]);

The ctx argument is explained in middleware's Context. The router methods can be imported in several ways:

// For whenever you have previously defined `server`
const { get, post } = server.router;

// For standalone files:
const { get, post } = require('server/router');

There are many more ways of importing the router methods, but those above are the recommended ones.

Complex routers

If you are going to have many routes, we recommend splitting them into separated files, either in the root of the project as routes.js or in a different place:

// app.js
const server = require('server');
const routes = require('./routes');

server(routes);
// routes.js
const { get, post } = require('server/router');
const ctrl = require('auto-load')('controllers');

// You can simply export an array of routes
module.exports = [
  get('/', ctrl.home.index),
  get('/users', ctrl.users.index),
  post('/users', ctrl.users.add),
  get('/photos', ctrl.photos.index),
  post('/photos', ctrl.photos.add),
  ...
];

The ctx variable is the context (documentation here). One important difference between the routes and middleware is that all routes are final. This means that each request will use one route at most.

All of the routers reside within the server.router and follow this structure:

const server = require('server');
const { TYPE } = server.router;
const doSomething = TYPE(ID, fn1, [fn2], [fn3]);
server(doSomething);

CSRF token

For POST, PUT and DELETE requests a valid CSRF token with the field name of _csrf must be sent as well. The local variable is set by server.js so you can include it like this:

<form action="/" method="POST">
 <input name="firstname">
 <input type="submit" value="Contact us">
 <input type="hidden" name="_csrf" value="{{csrf}}">
</form>

If you are using an API from Javascript, such as the new fetch() you can handle it this way:

<!-- within your main template -->
<script>
  window.csrf = '{{csrf}}';
</script>
// Within your javascript.js/bundle.js/app.js
fetch('/', {
  method: 'POST',
  body: 'hello world',
  credentials: 'include',  // Important! to maintain the session
  headers: { 'csrf-token': csrf }  // From 'window'
}).then(...);

Or you could also just disable it if you know what you are doing:

server({ security: { csrf: false } }, ...);

get()

Handle requests of the type GET (loading a webpage):

// Create a single route for GET /
const route = get('/', ctx => 'Hello δΈ–η•Œ');

// Testing that it actually works
run(route).get('/').then(res => {
  expect(res.body).toBe('Hello δΈ–η•Œ');
});

Note: Read more about the tests in code examples or just ignore them.

You can specify a query and param to be set:

const route = get('/:page', ctx => {
  console.log(ctx.params.page);  // hello
  console.log(ctx.query.name);   // Francisco
  return { page: ctx.params.page, name: ctx.query.name };
});

// Test it
run(route).get('/hello?name=Francisco').then(res => {
  expect(res.body).toEqual({ page: 'hello', name: 'Francisco' });
});

Handle requests of the type HEAD, which never contain a body:

// Create a single route for GET /
const route = head('/', ctx => 'Hello δΈ–η•Œ');

// Testing that it actually works
run(route).head('/').then(res => {
  // Body is empty
  expect(res.body).toBe('');
});

post()

Handle requests of the type POST. It needs a csrf token to be provided:

// Create a single route for POST /
const route = post('/', ctx => {
  console.log(ctx.data);
});

// Test our route. Note: csrf disabled for testing purposes
run(noCsrf, route).post('/', { body: 'Hello δΈ–η•Œ' });

The data property can be a string or a simple object of {name: value} pairs.

Example:

// index.js
const server = require('server');
const { get, post } = server.router;
const { file, redirect } = server.reply;

server(
  get('/', ctx => file('index.hbs')),
  post('/', ctx => {
    // Show the submitted data on the console:
    console.log(ctx.data);
    return redirect('/');
  })
);
<!-- views/index.hbs (omitting <head>, <body>, etc) -->
<form method="POST" action="/">
  <h2>Contact us</h1>
  <label><p>Name:</p> <input type="text" name="fullname"></label>
  <label><p>Message:</p> <textarea name="message"></textarea></label>

  <input type="hidden" name="_csrf" value="{{csrf}}">
  <input type="submit" name="Contact us">
</form>

Example 2: JSON API. To POST with JSON you can follow this:

fetch('/42', {
  method: 'PUT',
  body: JSON.stringify({ a: 'b', c: 'd' }),
  credentials: 'include', // !important for the CSRF
  headers: {
    'csrf-token': csrf,
    'Content-Type': 'application/json'
  }
}).then(res => res.json()).then(item => {
  console.log(item);
});

put()

Handle requests of the type "PUT". It needs a csrf token to be provided:

// Create a single route for PUT /ID
const route = put('/:id', ctx => {
  console.log(ctx.params.id, ctx.data);
});

// Test our route. Note: csrf disabled for testing purposes
run(noCsrf, route).put('/42', { body: 'Hello δΈ–η•Œ' });

The HTML <form> does not support method="PUT", however we can overcome this by adding a special field called _method to the query:

<form method="POST" action="/42?_method=PUT">
  ...
</form>

For Javascript you can just set it to method, for example using the new API fetch():

fetch('/42', {
  method: 'PUT',
  body: 'whatever',
  credentials: 'include', // !important for the CSRF
  headers: { 'csrt-token': csrf }
});

del()

Handle requests of the type "DELETE". It needs a csrf token to be provided:

// Create a single route for DELETE /ID
const route = del('/:id', ctx => {
  console.log(ctx.params.id);
});

// Test our route. Note: csrf disabled for testing purposes
run(noCsrf, route).del('/42');

The HTML <form> does not support method="DELETE", however we can overcome this by adding a special field called _method to the query:

<form method="POST" action="/42?_method=DELETE">
  ...
</form>

For Javascript you can just set it to method, for example using the new API fetch():

fetch('/42', {
  method: 'DELETE',
  credentials: 'include', // !important for the CSRF
  headers: { 'csrt-token': csrf }
});

error()

It handles an error thrown by a previous middleware:

const handle = error('special', ctx => {
  console.log(ctx.error);
});

// Test it. First let's define our error in a middleware:
const throwsError = ctx => {
  const err = new Error('This is a test error');
  err.code = 'special';
  throw err;
};

// Then test it faking a request
run(throwsError, handle).get('/');

It accepts an optional name and then middleware. If there's no name, it will catch all of the previously thrown errors. The name will match the beginning of the string name, so you can split your errors by domain:

// This will be caught since 'user' === 'user'
const mid1 = ctx => {
  const err = new Error('No username detected');
  err.code = 'user.noname';
  throw err;
};

// This will be caught since 'user.noname' begins by 'user'
const mid2 = ctx => {
  const err = new Error('No username detected');
  err.code = 'user.noname';
  throw err;
};

const handleUser = error('user', ctx => {
  console.log(ctx.error);
});

server(mid1, mid2, handleUser);

sub()

Handle subdomain calls:

const server = require('server');
const { sub } = server.router;

server([
  sub('es', ctx => {
    console.log('Call to subdomain "es"!');
  })
]);

It can be a string or a Regex:

const language = sub(/(en|es|jp)/, ctx => {
  console.log('Wikipedia <3');
});

socket()

Experimental now, coming stable in version 1.1

const server = require('server');
const { get, socket } = server.router;
const { render } = server.reply;

server({}, [
  get('/', ctx => render('/public/index.html')),

  // Receive a message from a single socket
  socket('message', ctx => {

    // Send the message to every socket
    io.emit('message', ctx.data);
  })
]);

Testing

If you happen to stumble here, this bit of the documentation is outdated and follows some old code. Please help us improve the project and the docs so we can make it into the official release.

There's a small test suite included, but you probably want to use something more specific to your use-case.

Testing that a middleware correctly handles the lack of a user:

// auth/errors.js (more info in /documentation/errors/)
const error = require('server/error');
error['/app/auth/nouser'] = 'You must be authenticated to do this';
module.exports = error;

Our main module:

// auth/needuser.js
const AuthError = require('./errors');

module.exports = ctx => {
  if (!ctx.user) {
    throw new AuthError('/app/auth/nouser', { status: 403, public: true });
  }
};

Then to test this module:

// auth/needuser.test.js
const run = require('server/test/run');
const needuser = require('./needuser');

describe('auth/needuser.js', () => {
  it('returns a server error without a user', async () => {
    const res = await run(needuser).get('/');
    expect(res.status).toBe(403);
  });

  it('works with a mocked user', async () => {
    const mockuser = ctx => { ctx.user = {}; };
    const res = await run(mockuser, needuser).get('/');
    expect(res.status).toBe(200);
  });
});

run()

This function accepts the same arguments as server(), however it will return an API that you can use to test any middleware (and, by extension, any route) that you want. The API that it returns so far is this:

const run = require('server/test/run');

const api = run(TOTEST);

api.get.then(res => { ... });
api.post.then(res => { ... });
api.put.then(res => { ... });
api.del.then(res => { ... });

Disable CSRF

For testing POST, PUT and DELETE methods you might want to disable CSRF. To do that, just pass it the appropriate option:

run({ security: { csrf: false } }, TOTEST);

This API accepts as arguments:

api.get(URL, OPTIONS);

It is using request underneath, so the options are the same as for this module. There are few small differences:

  • It will generate the port randomly from 1024 to 49151. However, there is a chance of collision that grows faster than expected as your number of tests grows. There's mitigation code going on to avoid collisions so until the tens of thousands of tests it should be fine.
  • The URLs will be made local internally to http://localhost:${port} unless you fully qualify them (which is not recommended).
Source code

Real-time chat

In this tutorial you will learn how websockets work, the specifics of socket.io and how to create a real-time chat with server.js.

Make sure to follow the getting started tutorial first. We won't use any database, so there is no chat history, just real time chat.

This tutorial is a beginner introduction. However, the socket.io plugin is experimental right now so please don't use this in production (or at least lock the version down). Also, there are absolutely no security measures now since this is a proof of concept.

User Interface

First we are going to create a user interface that looks something like this:

Tokyo Chat

In your project folder create a folder public and put the file index.html inside:

<!-- ./public/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>First website</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="style.css">
  </head>
  <body>

    Hello world

    <!-- Include jquery, cookies, socket.io (client-side) and your own code -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
    <script src="https://unpkg.com/[email protected]/cookie.min.js"></script>
    <script src="https://unpkg.com/socket.io-client@2/dist/socket.io.slim.js"></script>
    <script src="javascript.js"></script>
  </body>
</html>

This is the basic, fairly standard HTML skeleton of your chat. We are including few libraries that we will be using later on. Save it, open the file in your browser and you should see "Hello World".

Now let's make the actual interface. We will wrap everything with a <main> tag for the general layout, then put the different elements inside. This code goes in the place of Hello world in the skeleton above:

<main>
  <header>
    <div class="user-count">0</div>
    <h1>Tokyo Chat</h1>
  </header>

  <section class="chat">
    <p><strong>Pepito</strong>: Hey everyone!</p>
    <p><strong>θ¦ͺζ—₯</strong>: こんいけは!</p>
  </section>

  <form>
    <input type="text" placeholder="Say something nice" />
    <button>Send</button>
  </form>
</main>

The focus of this tutorial is not to teach HTML+CSS, so for now let's copy/paste the CSS into ./public/style.css:

/* ./public/style.css */
* {
  box-sizing: border-box;
  transition: all .3s ease;
}

html, body {
  background: #eee;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

main {
  width: calc(100% - 20px);
  max-width: 500px;
  margin: 0 auto;
  font-family: Helvetica, Arial, Sans, sans-serif;
}

h1, .user-count {
  margin: 0;
  padding: 10px 0;
  font-size: 32px;
}

.user-count {
  float: right;
}

.chat {
  content: '';
  width: 100%;
  height: calc(100vh - 165px);
  background: white;
  padding: 5px 10px;
}

.chat p {
  margin: 0 0 5px 0;
}

input, button {
  width: 100%;
  font: inherit;
  background: #fff;
  border: none;
  margin-top: 10px;
  padding: 5px 10px;
}

button:hover {
  cursor: pointer;
  background: #ddd;
}

@media all and (min-width: 500px) {
  .chat {
    height: calc(100vh - 140px);
  }
  input {
    width: calc(100% - 160px);
  }
  button {
    float: right;
    width: 150px;
  }
}

You can see an example of this interface in this JSFiddle. It is responsive and has many small details, so it should work smoothly on any (modern) device. It looks like this:

Screenshot of chat

Choose a username

Before doing anything else let's get the visitor username. Create a file called javascript.js inside the folder public with this:

// /public/javascript.js

// Get the current username from the cookies
var user = cookie.get('user');
if (!user) {

  // Ask for the username if there is none set already
  user = prompt('Choose a username:');
  if (!user) {
    alert('We cannot work with you like that!');
  } else {
    // Store it in the cookies for future use
    cookie.set('user', user);
  }
}

It will try to retrieve the username from the cookies. If there is none, it will ask for the username with a standard system prompt like this:

Screenshot of chat with the prompt

Now that we have the username stored in the cookies, let's see how to communicate with websockets.

Sending messages

Websockets is a web technology for real time, bidirectional communication from the browser to the server and back. This has traditionally been a hard problem, with the browser having to poke every X seconds to the server to ask for new data.

The most commonly used library is socket.io since it makes it a lot easier to use the underlying technology. We will use the client library (already included in our skeleton HTML code) for the browser. First, let's connect to the server:

// Connect to the server-side websockets. But there's no server yet!
var socket = io();

Then we will be sending and receiving messages. Let's handle the receiving messages first with socket.on(TYPE, callback):

// The user count. Can change when someone joins/leaves
socket.on('count', function (data) {
  $('.user-count').html(data);
});

// When we receive a message
// it will be like { user: 'username', message: 'text' }
socket.on('message', function (data) {
  $('.chat').append('<p><strong>' + data.user + '</strong>: ' + data.message + '</p>');
});

Finally, let's send some data when our form is submitted with socket.emit():

// When the form is submitted
$('form').submit(function (e) {
  // Avoid submitting it through HTTP
  e.preventDefault();

  // Retrieve the message from the user
  var message = $(e.target).find('input').val();

  // Send the message to the server
  socket.emit('message', {
    user: cookie.get('user') || 'Anonymous',
    message: message
  });

  // Clear the input and focus it for a new message
  e.target.reset();
  $(e.target).find('input').focus();
});

Awesome, if you have followed all along this is the final code for javascript.js:

// ./public/javascript.js

// Get the current username from the cookies
var user = cookie.get('user');
if (!user) {

  // Ask for the username if there is none set already
  user = prompt('Choose a username:');
  if (!user) {
    alert('We cannot work with you like that!');
  } else {
    // Store it in the cookies for future use
    cookie.set('user', user);
  }
}

var socket = io();

// The user count. Can change when someone joins/leaves
socket.on('count', function (data) {
  $('.user-count').html(data);
});

// When we receive a message
// it will be like { user: 'username', message: 'text' }
socket.on('message', function (data) {
  $('.chat').append('<p><strong>' + data.user + '</strong>: ' + data.message + '</p>');
});

// When the form is submitted
$('form').submit(function (e) {
  // Avoid submitting it through HTTP
  e.preventDefault();

  // Retrieve the message from the user
  var message = $(e.target).find('input').val();

  // Send the message to the server
  socket.emit('message', {
    user: cookie.get('user') || 'Anonymous',
    message: message
  });

  // Clear the input and focus it for a new message
  e.target.reset();
  $(e.target).find('input').focus();
});

Server handling

This library is included by default from server, so let's take advantage of it! First we create a simple server that will render our HTML page. Create a file in the root of your project called index.js:

// /index.js

const server = require('server');
const { get, socket } = server.router;
const { render } = server.reply;

server([
  get('/', ctx => render('index.html'))
]);

We can run in the terminal node . and access to localhost:3000 to see our chat interface.

Then we will add connect and disconnect routes. We want to update everyone with the current amount of users when someone joins or leaves. We can use the same function for both of them that will send a message to everyone with socket.io's io.emit():

// /index.js

const server = require('server');
const { get, socket } = server.router;
const { render } = server.reply;

const updateCounter = ctx => {
  ctx.io.emit('count', ctx.io.sockets.sockets.length);
};

server([
  // For the initial load render the index.html
  get('/', ctx => render('index.html')),

  // Join/leave the room
  socket('connect', updateCounter),
  socket('disconnect', updateCounter)
]);

Finally let's create a new socket router that, when it receives a message, it will push the same message to everyone in a similar way as before:

// /index.js

const server = require('server');
const { get, socket } = server.router;
const { render } = server.reply;

// Update everyone with the current user count
const updateCounter = ctx => {
  ctx.io.emit('count', Object.keys(ctx.io.sockets.sockets).length);
};

// Send the new message to everyone
const sendMessage = ctx => {
  ctx.io.emit('message', ctx.data);
};

server([
  get('/', ctx => render('index.html')),
  socket('connect', updateCounter),
  socket('disconnect', updateCounter),
  socket('message', sendMessage)
]);

Great, now we have our back-end. Launch it by running this in your terminal:

node .

Access localhost:3000 in two different tabs. You can now talk with yourself in realtime!

Final version

User X joined

Exercise: add a new socket route and the corresponding back-end and front-end code to display a new message when a user writes their username.

Tip: send an event called join from the front-end when a user writes their username with socket.emit('join', ...).

Tip 2: add a route in the back-end for this event that will emit the same to everyone.

Tip 3: add a handler socket.on('join', ...) for this event in the front-end in a similar fashion to the socket.on('message', ...).

Upload to Heroku

Exercise: upload our chat to Heroku so several people can use it.

Tip: make sure to specify the engine in package.json, since we want for heroku to use Node.js 7.6.0 or newer.

XSS Protection

Exercise: what happens when we write <script>;alert('Hello world!');</script> as a message? Why is this dangerous? Please fix this issue.

Extra: did you fix the visitor writing the same code in their username? Make sure this is also sanitized.

Getting started

In this tutorial you will learn how to get started with Node.js development and create a project from scratch. While there are many ways of doing it, this guide is focused first on making it easy and second on using common tools.

You will need some basic tools like having git and a code editor (we recommend Atom) as well as some basic knowledge around your operative system and the terminal/command line.

Install Node.js

This will largely depend on your platform and while you can just download the binary program from the official page I would recommend using Node Version Manager for mac or Linux:

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash

After this, we have to close and open the terminal for it to work properly. Then we use NVM to install a current Node.js' version in the terminal:

nvm install node
nvm use node
nvm alias default node

Server requires Node.js 7.6.0 or newer. Node.js 8.9.x LTS is recommended for long-term support from Node.js.

Great, now we should have Node.js installed and working. To test it's properly installed, execute this on the terminal:

node --version    # Should show the version number over 8.x
npm --version     # verify this is installed as well over 5.x

Create your project

Now that we have Node.js installed, let's get started with one project. Create a folder in your computer and then access it from the terminal. To access it use cd:

cd "./projects/My Cool Project"

From now on, all the operations from the terminal must be executed while inside your project in said terminal.

Then open the project from Atom or your code editor: File > Add project folder... > My Cool Project.

Initialize Git and npm

Git will be used for handling your code, deploying it and working collaboratively. To get your git started execute this in your terminal:

git init

It will create a folder called .git with all the version history in there. We highly recommend to create a file called .gitignore (yes, a point and the name with no extension) where we add at least the following:

*.log
npm-debug.log*
node_modules
.env

This is because we want these files and places to only be accessible from our computer, but we don't want to deploy them along the rest of the code.

Finally we will initialize our project by doing init in the terminal:

npm init

It will ask some questions, just answer them or press the "enter" key to accept the default (set the "main" to "index.js"). After answering everything you should have a package.json file, so now you can edit the part where it says "scripts" to add this:

  "scripts": {
    "start": "node index.js",
    "test": "jest --coverage --forceExit"
  },

Make awesome things!

That is great! Now you can install the packages that you want like server.js:

npm install server

And then create a file called index.js with the demo code to see how it works:

// Import the library
const server = require('server');

// Launch the server to always answer "Hello world"
server(ctx => 'Hello world!');

To execute it after saving it, run from the terminal:

npm start

And finally open http://localhost:3000/ to see it in action!

Note: this guide was published originally on Libre University - Getting started but has since been adapted better only for Node.js.

Session in production

Sessions work out of the box for developing, but they need a bit of extra work to be ready for production.

Secret

The first thing to change is adding a session secret as an environment variable in .env for your machine:

SECRET=your-random-string-here

This will be used to secure the cookies as well as for other plugins that need a secret. Make it unique, long and random. Then don't forget to add a different one for the production server and other stages in your deploy pipeline if any. Also, exclude the .env file from Git as explained here.

Storage

By default sessions work in-memory with server so they are not ready for production:

// Simple visit counter for the main page
const counter = get('/', ctx => {
  ctx.session.views = (ctx.session.views || 0) + 1;
  return { views: ctx.session.views };
});

/* test */
await run(counter).alive(async api => {
  let res = await api.get('/');
  expect(res.body.views).toBe(1);
  res = await api.get('/');
  expect(res.body.views).toBe(2);
  res = await api.get('/');
  expect(res.body.views).toBe(3);
});

This works great for testing; for quick demos and for short sessions, but all session data will die when the server is restarted since they are stored in the RAM.

To make them persistent we recommend using a compatible session store. We bundle Redis for Node.js by default, so you just have to install it (*nix systems have it easily available). For example, on Ubuntu:

sudo apt install redis-server

Then edit your .env to include REDIS_URL:

SECRET=your-random-string-here
REDIS_URL=redis://:password@hostname:port/db_number

Note: for Heroku this variable is created automatically when adding the appropriate add-on. For other hosting companies please consult their documentation.

Otherwise add your preferred store to the session through the options:

const server = require('server');
// Your own file for the config:
const store = require('./session-store.js');
server({ session: { store } }, [
  // Routes here
]);

Alternatives

Why not just use cookie-session? Here is an explanation of the alternative, but it boils down to:

  • They are more insecure, since all the session data (including sensitive data) is passed forward and backward from the browser to the server in each request.
  • If the session data is large then that means adding an unnecessary load to both the server and the browser.
Source code

Spreadsheet database

In this tutorial you'll learn how to take a Google Spreadsheet and convert it into a small database for Node.js. We will see some possible applications such as graph statistics and geolocation.

This is useful for simple and small datasets that might be modified by non-technical people since everyone knows how to edit a spreadsheet. The data will then be available on the site as it is modified by several people simultaneously on Google Drive.

Some possible uses:

  • Publish a graph with monthly revenue or users info that you are already using internally.
  • Startup employee list is in a Spreadsheet for internal use and visible on the website.
  • Keep track of your trips similar to how Martin does it (note: not related to server.js).

Create a spreadsheet

First we'll have to go to Google Spreadsheets and create a new Blank spreadsheet:

Create new spreadsheet in Google Spreadsheets

The spreadsheet has to have a specific structure, where the first row has the names of the columns and the rest has the content:

Random userlist example

Now we have to publish the spreadsheet so that we can read it from our back-end. Press File > Publish to the web > Publish:

Publish the spreadsheet to the web

Finally make a note of the current spreadsheet URL. It will be something like this: https://docs.google.com/spreadsheets/d/1FeG6UsUC_BKlJBiy3j_c4uctMcQlnmv9iyfkDjuWXpg/edit

Installation

After [getting your project started](https://en.libre.university/lesson/V1f6Btf8g/Getting started), we'll be using the packages server and drive-db:

npm install server drive-db

Back-end with server.js

Let's get to the code. Create the entry file index.js where the main logic will reside:

// Load the dependencies
const server = require('server');
const { render } = server.reply;

// The URL fragment between "spreadsheets/d/" and "/edit"
const id = '1FeG6UsUC_BKlJBiy3j_c4uctMcQlnmv9iyfkDjuWXpg';
const drive = require('drive-db')(id);

// Launch the server in port 3000
server(async () => {

  // Local or remote (depends on the cache expiration)
  const db = await drive.load();

  // Render the index with the user data
  return render('index.hbs', { users: db.find() });
});

Since this is a fairly simple back-end, everything you will need for the back-end is in this file. It loads the libraries, sets up a route and launches the server.

Front-end

Now let's do some fun stuff with the front-end. Create the file views/index.hbs with this basic structure:

<!DOCTYPE html>
<html>
<head>
  <title>Spreadsheet Tutorial</title>
  <link rel="stylesheet" href="https://unpkg.com/picnic@6">
  <style>
    main { width: 90%; max-width: 800px; margin: 30px auto; }
    table, canvas, img { width: 100%; }
    h2 { padding-bottom: 20px; }
    .card { margin: 0; }
    #map { height: 400px; }
  </style>
</head>
<body>
  <main>
    <h1>Users</h1>

    ...

  </main>
</body>

We added Picnic CSS to make it easier to develop, however you can use any front-end CSS library that you prefer like Bootstrap or write your own CSS. We will see several ways of representing our data next.

Simple list

The easy way, create a list with the first name and last name:

Simple user list

To achieve this we are using Handlebars with some {{#each ...}} looping:

<h1>Simple list</h1>
<ul class="flex one three-800">
  {{#each users}}
    <li>{{this.firstname}} {{this.lastname}}</li>
  {{/each}}
</ul>

The classes on the <ul> come from Picnic CSS grid system.

Table

Let's show them in a table as they appear in the Spreadsheets:

Table list

If you already know the basics of HTML you'll have guessed this correctly, we are using the native <table> element to display it. First we set up the headers and then the content, again looping with an {{#each ...}} loop from Handlebars:

<table>
  <tr>
    <th>Firstname</th> <th>Lastname</th> <th>Age</th> <th>City</th>
  </tr>
  {{#each users}}
    <tr>
      <td>{{this.firstname}}</td>
      <td>{{this.lastname}}</td>
      <td>{{this.age}}</td>
      <td>{{this.city}}</td>
    </tr>
  {{/each}}
</table>

Cards

Now let's try showing them as a group of cards such as Airbnb or Dribbble. This is a great way of showcasing creative content:

Card list

Our hbs code is a small grid system and some cards, each with its image and footer:

<h2>Cards</h2>

<div class="flex one three-900">
  {{#each users}}
    <div>
      <div class="card">
        <img src="http://lorempixel.com/400/200/?{{this.id}}">
        <footer>{{this.firstname}} {{this.lastname}}, {{this.age}}</footer>
      </div>
    </div>
  {{/each}}
</div>

In the real world those images would be stored somewhere, and the lorempixel link would still be pointing to the image reference. These card and grid styles come from Picnic CSS.

Demographics by age

Maybe you have a fairly longer dataset and want to group them by age and display this into a graph like the following:

Demographics chart

First we will define where we want to include the chart. This will be a small HTML placeholder, since the chart library that we are using works with <canvas>:

<h2>Demographics by age:</h2>
<canvas id="people" width="800" height="300"></canvas>

For the main logic we will be using javascript. We need to include the library Chart.js and inject our users into the Javascript:

<script src="https://unpkg.com/chart.js/dist/Chart.bundle.min.js"></script>
<script>
  // Create the variable and inject the different parts
  var users = [
    {{#each users}} {
      id: {{{this.id}}},
      firstname: "{{this.firstname}}",
      lastname: "{{this.lastname}}",
      age: {{this.age}},
      city: "{{this.city}}",
    }, {{/each}}
  ];

  // ...
</script>

Now that we have the data, in the ... previously we will write our javascript code to create the chart:

// Get the place where the chart will be drawn
var ctx = document.getElementById("people").getContext('2d');

// Initialize the chart into a variable
var myChart = new Chart(ctx, {
  type: 'bar',
  data: {
    // Group the data by age ranges by filtering those
    labels: ["0-19", "20-39", "40-59", "60+"],
    datasets: [{
      data: [
        users.filter(function(u){ return u.age < 20 }).length,
        users.filter(function(u){ return u.age >= 20 && u.age < 40 }).length,
        users.filter(function(u){ return u.age >= 40 && u.age < 60 }).length,
        users.filter(function(u){ return u.age >= 60 }).length
      ],
      backgroundColor: [
        'rgba(255, 99, 132, 0.5)',
        'rgba(54, 162, 235, 0.5)',
        'rgba(255, 206, 86, 0.5)',
        'rgba(75, 192, 192, 0.5)',
      ]
    }]
  },
  options: {
    legend: { display: false },
    scales: { yAxes: [{ ticks: { beginAtZero: true } }] }
  }
});

Demographics by location

Finally let's geolocate each user into a map:

Demographics map

First we have to create our html placeholder, where the Javascript will position the map. This time it will be a simple div with the id map:

<h2>Demographics by location:</h2>
<div id="map"></div>

Now we have to create the javascript variable users, see the previous point to find out how to do this. Then we create the map and position the markers:

function initMap() {
  var map = new google.maps.Map(document.getElementById('map'), {
    zoom: 4,
    center: { lat: 39, lng: -100 }
  });

  var geocoder = new google.maps.Geocoder();
  users.forEach(function(user) {
    var newAddress;
    geocoder.geocode({ address: user.city + ', United States' }, function(res){
      var marker = new google.maps.Marker({
        position: res[0].geometry.location,
        map: map,
        title: user.firstname
      });
    });
  });
}

And finally we have to include Google Maps. You will have to include your own Google Maps API key here where it says YOUR_API_KEY:

<script async defer src="https://maps.googleapis.com/maps/api/js?libraries=places&key=YOUR_API_KEY&callback=initMap"></script>

This will call the function initMap() when the script is loaded, and effectively create your map with all of the markers.

Source code

TO-DO list

In this tutorial you will learn to design a basic API to create a list of items. We store them in a MongoDB database using Mongoose and it will be for a single person.

Some possible uses:

  • An actual TO-DO list. Some times you just need a simple list.
  • The beginning of Hacker News, Reddit, or similar. Those are basically four glorified CRUDs: users, stories, comments, votes.

End product:

Screenshot of the final project

Install dependencies

After getting your project ready you'll have to make sure that you have MongoDB installed following the official guide and run it (will depend on your installation process). To check that you have it on Ubuntu do:

mongod --version   # Should display a number
mongod

Then we install the two libraries that we will be using within our project folder:

npm install server mongoose jest

Code organization

Since this is a fairly small project focused on the back-end we will have all our server files within the root folder and won't go into detail for the front-end. The project will have these files:

  • public/: the folder for public assets.
  • views/: the folder for the only view.
  • index.js: the entry point and routers.
  • model.js: the definition of the database structure.
  • package.json: npm package where the dependencies and some info is.
  • test.js: integration tests to make sure everything is working.
  • todo.js: interaction with the database and main logic.

You can see the whole working project in the repository:

Github Repository

REST API

Let's first of all define our API. Let's keep it simple! Within index.js we write:

// index.js
const server = require('server');
const { get, post, put, del } = server.router;
const { render } = server.reply;
const todo = require('./todo.js');

// Render the homepage for `/`
const home = get('/', ctx => render('index.hbs'));

// Add some API endpoints
const api = [
  get('/todo', todo.read),
  post('/todo', todo.create),
  put('/todo/:id', todo.update),
  del('/todo/:id', todo.delete)
];

// Launch the server with those
server(home, api);

This first loads the needed library and functions, then defines few routes and finally launches the server with those routes. We are using the default settings so no options are needed.

We are using the CRUD operation names, but any of those is fairly common for the method names (just keep them consistent):

  • get(): read, retrieve, all, list, select
  • post(): create, add, insert
  • put(): edit, update, change, modify
  • del(): delete, remove, destroy

Database

We are using Mongoose (a layer on top of MongoDB) to implement database access. For this, we first have to create a small model.js where we define how our schema and model data looks like:

// model.js
const mongoose = require('mongoose');

// Configure the Mongoose plugin
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost/todo');

// define the Todo schema
const TodoSchema = mongoose.Schema({
  text: { type: String, required: true },
  done: { type: Boolean, required: true, default: false },
});

module.exports = mongoose.model('Todo', TodoSchema);

This way, all of our TODOs will have two fields, the text and a boolean indicating whether or not it's done.

The mongoose configuration MONGODB_URI comes from the environment.

Todos logic

Now, to write this code we create the file todo.js with this code:

// todo.js
const { json } = require('server/reply');
const model = require('./schema');

export.read = async ctx => {};
export.create = async ctx => {};
export.update = async ctx => {};
export.delete = async ctx => {};

These are the 4 basic CRUD operations as shown before. This file will export these asynchronous functions, that will take the context argument as with any server.js middleware and whatever they return will be used for the response.

Finally, let's implement the database access logic inside each of these 4 functions:

// todo.js
const { status, json } = require('server/reply');
const Todo = require('./model');

exports.read = async (ctx) => {
  return Todo.find().sort('done').lean().exec();
};

exports.create = async (ctx) => {
  const item = new Todo({ text: ctx.data.text });
  return status(201).json(await item.save());
};

exports.update = async (ctx) => {
  const set = { $set: { done: ctx.data.done } };
  await Todo.findByIdAndUpdate(ctx.params.id, set).exec();
  return Todo.find().sort('done').lean().exec();
};

exports.delete = async (ctx) => {
  return Todo.findByIdAndRemove(ctx.params.id).exec();
};

Testing

This section describes a future API and **it is not available yet**. Now please use more traditional testing method.

We will be using Jest for testing, but you can use any library or framework that you prefer. We have to make a small change in our main index.js: we export the return value from server():

// ...
module.exports = server(home, api);

Then we can import it from the integration tests. Let's create a test.js:

// test.js
const run = require('server/test/run');
const server = require('./index.js');

describe('Homepage', () => {
  it('renders the homepage', async () => {
    const res = await run(server).get('/');
    expect(res.status).toBe(200);
    expect(res.body).toMatch(/\<h1\>TODO list<\/h1>/i);
  });
});