Using RethinkDB with Express JS
RethinkDB is an easy to
use JSON database written from the ground up for the realtime web. This
article will introduce you to the basics of using it with Express JS to build an API. As an added bonus we’ll also learn about JSON Web Tokens. A complete example
is available on Github if you prefer to just read the code. Actually
you should read it anyway so you can see the code in context.
Please make sure you have RethinkDB installed and running on your system by following the instructions in the official documentation for your operating system. At the time of writing, Windows is not a support platform.
Starting an Express JS application
To get started writing an Express JS application we first need to install our dependencies from npm. Generate a
package.json
file by opening a terminal prompt in the applications root directory and type npm init
.
This will ask you a series of questions about the application such as
the name, version number and the type of license to use. I usually go
with the defaults and edit the file once it has been generated. In the
example below I've added "private": true
so it doesn't accidentally get published to npm and replaced the "test"
line inside scripts with "start": "node app.js"
.{
"name": "express_rethink",
"version": "0.0.2",
"description": "An Express JS and RethinkDB example",
"main": "app.js",
"private": true,
"scripts": {
"start": "node app.js"
},
"author": "Craig Walsh",
"license": "MIT"
}
Next we install the dependencies. Notice I've used the
--save
argument at the end of the command. This will write the module name and version number to the package.json
file so in the future, all we need to do is type npm install
to get the dependencies.npm install bcrypt bluebird body-parser cors dotenv express helmet jwt-simple moment morgan rethinkdb --save
You should now see a new folder called
node_modules
in the applications root directory. This is where all the software we
have just installed is kept. It's good practice to ignore this directory
in your version control system.The main application file
When we type
npm start
to run the application, it's going to look for a file called app.js
to tell it what to do. Create a file called app.js
and require the modules needed, then assign an instance of the Express application server to a variable called app
.var express = require('express');
var logger = require('morgan');
var bodyParser = require('body-parser');
var cors = require('cors');
var helmet = require('helmet');
require('dotenv').load();
var app = express();
Now we call the use
method on our application instance
to configure the modules we required. Make sure you read the
documentation for any module you plan to use with Express JS so you know
how it's configured.app.use(logger('dev'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cors());
app.use(helmet());
In the last section, there is some error handling code and a
server definition so that Express JS knows what port to serve the
application on.
app.use(function (error, request, response, next) {
response.status(error.status || 500);
response.json({ error: error.message });
});
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('App is listening on http://%s:%s', host, port);
});
Express JS routes
The next job is to add endpoints so the user can interact with our API. Create a folder in the root directory called
routes
. Inside, create two files called users.js
and login.js
. It's in these files we'll write the applications route definitions but before that, we need to link to them from the app.js
file.var users = require('./routes/users');
var login = require('./routes/login');
app.use('/users', users);
app.use('/login', login);
Assign the files we created to a variable and then call
use
again on the app
instance, passing in a URL and the assigned variable. The URL will be
what the user types into the address bar in the browser to gain access
to the routes in that file.
Now to define the routes themselves. Start by requiring
express
and assigning an instance of the express.Router()
to a variable. We can call get
, post
, put
and delete
(to name the popular ones) on the router instance to define routes. Here I'm calling get
to write "Hello World" to the screen.var express = require('express');
var router = express.Router();
router.get('/', function (request, response) {
response.send("Hello World!");
});
To learn more about routing in Express JS, take a look the the official documentation.
Configuring RethinkDB
To configure RethinkDB, the first thing we need to do is create the database and users table. This can be automated with scripts but I’m going to show you how to do it using the RethinkDB Administration Console.
Open a browser and navigate to
To enable the application to interact with the database, we need to
supply some information about where to find the database server, what
port it’s running on and the name of the database we’re working with.
Create a file called http://localhost:8080
. Click on the Tables
section at the top. You should see the test database listed. Click on the Add Database
button and follow the on screen instructions to create a database named express_rethink
. Now click on the Add Table
button and follow the same procedure to create a table called users
.database.js
in the config
directory.module.exports = {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
db: process.env.DB_NAME
};
We’re using environment variables to store database information
because it’s not a good idea to have it available for everyone to see
(more on this later). Using the dotenv
module we installed earlier, we can read this information from a file called .env
in the root directory.DB_HOST=localhost
DB_PORT=28015
DB_NAME=express_example
ReQL query library
To make the code more readable we’ll put everything needed
to run queries against the database in a separate file. This way we can
just drop the file into other projects using RethinkDB and only have to
maintain one codebase.
Using Rethink’s query language
(ReQL) we can make function calls using Javascript to get information
out of the database. The queries are chainable, so it’s not unlike
writing Node code, and executed on the server once you call run
and pass in an active connection.
Create a file in the
lib
directory called rethink.js
. Require the rethink
module and the configuration information we wrote in the previous section, then use rdb.connect
to open a connection to the database. Inside that block, write a function called find
that passes in a table name and id. We’ll use this information to build
the query, run it on the server and the return the result to the
calling function.var rdb = require('rethinkdb');
var dbConfig = require('../config/database');
var connection = rdb.connect(dbConfig)
.then(function (connection) {
module.exports.find = function (tableName, id) {
return rdb.table(tableName).get(id).run(connection)
.then(function (result) {
return result;
});
};
});
Take a look at the other queries I’ve included in the Github example
to perform common CRUD operations. Notice that when the query finds
more than one result it returns a cursor which much be converted to an
array so it can be parsed. A great place to find common tasks and
queries is in the Cookbook section on Rethink’s official documentation.
Authenticating users
In order to Authenticate users we need to store details about their name, email address and password. It’s our job to be responsible about what we do with the information entrusted to us, so we will only store an encrypted version of their password.
The first thing we need to do is write the function that
will encrypt the users password. Before we do though, there is an
interesting problem that needs some further explanation.
If you use the code found in the bcrypt
documentation
to encrypt the password, the return value will not be what you expect
it to be. Because we have separated our database code out into a file of
it’s own we need to return the hashed password instead of saving it
there and then, but the save
function will have completed before the hash_password
function has a chance to return a value.The solution is to wrap the function in a promise. If you haven’t come across promises yet I highly recommend reading this HTML5 Rocks article. Basically, when the
save
function calls the hash_password
function, hash_password
promises to return a value and save
waits for the promise to be resolved.Create a file in the
lib
directory called auth.js
and require the bcrypt
module. We will also require the bluebird
module because promises aren’t available in all browsers yet. Write the hash_password
function passing in the submitted password and fill it in with our promisified bcrypt code.var bcrypt = require('bcrypt');
var Promise = require('bluebird');
module.exports.hash_password = function (password) {
return new Promise(function (resolve, reject) {
bcrypt.genSalt(10, function (error, salt) {
if(error) return reject(error);
bcrypt.hash(password, salt, function (error, hash) {
if(error) return reject(error);
return resolve(hash);
});
});
});
};
We also need an authenticate
function that passes in the
submitted password and the hash stored in the database for comparison.
The bcrypt section is again promisified.module.exports.authenticate = function (password, hash) {
return new Promise(function (resolve, reject) {
bcrypt.compare(password, hash, function (error, response) {
if(error) return reject(error);
return resolve(response);
});
});
};
Authorising users
To secure the API further we are using JSON Web Tokens. This means that when a user successfully authenticates, they are issued a token which must be attached to the headers of every request to a protected route. If the token is not found, or is invalid, access is denied. For a more in depth explanation take a look at this Smashing Magazine article.Create a file in the
lib
directory called token.js
. Require the jwt-simple
module and moment
library. We also need to have a secret string that the jwt-simple
module will use to generate a unique token.var jwt = require('jwt-simple');
var moment = require('moment');
var secret = process.env.TOKEN_SECRET;
Just like the database information, this should not be available for everyone to see, so add it to the .env
file we created earlier.TOKEN_SECRET=mysupersecretstring
Write a function called generate
that passes in the authenticated user. With the moment
module, we create a date at some point in the future (I've used 7 days
but you can use any length of time you want) that will get encoded into
the token so that it expires after the given number of days. Using the
expiry date, the users email address and the secret string the encode
method generates a token that is returned to the calling function.module.exports.generate = function (user) {
var expires = moment().add(7, 'days').valueOf();
return jwt.encode({ iss: user.email, exp: expires }, secret);
};
When the client application submits the token with a request, we need
to verify that it is valid and has not expired. To do this, write a
function called verify
that passes in the submitted token.
If the token does not exist or is not valid an error is generated,
otherwise the request is allowed to continue.module.exports.verify = function (token, next) {
if(!token) {
var notFoundError = new Error('Token not found');
notFoundError.status = 404;
return next(notFoundError);
}
if(jwt.decode(token, secret) <= moment().format('x')) {
var expiredError = new Error('Token has expired');
expiredError.status = 401;
return next(expiredError);
}
};
Now that we have a way to generate and verify tokens, let’s write the
middleware that will authorise the routes we want to protect. For this
we need to require the token library we wrote in the last section then
simply grab the token from the headers, verify that it’s valid and move
on to the next middleware in the stack.var token = require('./token');
module.exports.authorize = function (request, response, next) {
var apiToken = request.headers['x-api-token'];
token.verify(apiToken, next);
next();
};
The final step in this section is to choose which routes we want to protect. After requiring the
auth.js
library, call the authorize
function in between the route path and callback. This causes the authorize
function to run before processing the rest of the route.var auth = require('../lib/auth');
router.get('/', auth.authorize, function (request, response) {
rdb.findAll('users')
.then(function (users) {
response.json(users);
});
});
Receiving login details
Earlier we created an endpoint where users could post their
login details. The email address is used to find the user in the
database. If no user is found, an error is generated and the request
ends. If the user is found, the submitted password and the hashed
password from the user record are passed to the
authenticate
method for comparison. If authentication is successful the user is returned a currentUser
object containing some general information about the user and their access token.var express = require('express');
var rdb = require('../lib/rethink');
var auth = require('../lib/auth');
var token = require('../lib/token');
var router = express.Router();
router.post('/', function (request, response, next) {
rdb.findBy('users', 'email', request.body.email)
.then(function (user) {
user = user[0];
if(!user) {
var userNotFoundError = new Error('User not found');
userNotFoundError.status = 404;
return next(userNotFoundError);
}
auth.authenticate(request.body.password, user.password)
.then(function (authenticated) {
if(authenticated) {
var currentUser = {
name: user.name,
email: user.email,
token: token.generate(user)
};
response.json(currentUser);
} else {
var authenticationFailedError = new Error('Authentication failed');
authenticationFailedError.status = 401;
return next(authenticationFailedError);
}
});
});
});
Security considerations
When writing any application for the Internet, security
needs to be a major consideration. Email addresses and passwords will by
flying around in plain text so it's important to make sure an API like
the one we've just been discussing is served over an HTTPS connection.
You also need to ensure there is no sensitive data stored in the source
code you have published. It's good practice to store information such as
secret strings and API keys in environment variables. Oh, and try not
to use the same secret string for every application!
Post a Comment