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:
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');
});
Sponsor
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 + β₯ |
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!
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:
.env
: the variable within the environment.server({ OPTION: 3000 })
: the variable set as a parameter when launching the server.- 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 unusablealert
: action must be taken immediatelycritical
: the system is in critical conditionerror
: error conditionwarning
: warning conditionnotice
: a normal but significant conditioninfo
: a purely informational messagedebug
: 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 inserver(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 whendefault
is set.type: false
: define the type of the variable. A type or an array of types. Will only check the primitive typesBoolean
,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 returntrue
for a valid value orfalse
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:
- cookie(): add cookie headers
- header(): add any headers you want
- status(): set the status of the response
- type(): adds the header 'Content-Type'
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.
cookie()
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); })
]);
header()
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' });
});
head()
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).
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:
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:
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:
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!
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.
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:
The spreadsheet has to have a specific structure, where the first row has the names of the columns and the rest has the content:
Now we have to publish the spreadsheet so that we can read it from our back-end. Press File
> Publish to the web
> Publish
:
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:
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:
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:
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:
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:
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.
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:
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 RepositoryREST 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);
});
});