Build And Share Your Single-page App With React, Node and Pouchdb, part III
Table of contents
- Description of the main concepts of single page applications
- How to build your client with ReactJS and Browserify
- How to build your REST API with ExpressJS, Americano and PouchDB
- Share and deploy your web app with NPM
How to build your REST API with ExpressJS, Americano and PouchDB
In the first part, we mentioned the motivations behind this tutorial. The second was dedicated to the development of the client. Now, we are going to focus on the server side. We are going to build the REST API required by the client to persist the data.
Technologies involved:
User stories
Before going further let's write our user stories:
- As a HTTP client, I want to be able to create new bookmarks.
- As a HTTP client, I want to be able to retrieve my bookmark list.
- As a HTTP client, I want to be able to delete a given bookmark.
- As a browser, I want to load the entry point of the frontend.
Now let's break this down in more precise stories:
- When I send a GET request to
/bookmarks
, it loads the bookmark list at JSON format. - When I send a POST request to
/bookmarks
with required fields at JSON format, it creates and persist the bookmark. - When I send a DELETE request to
/bookmarks/:id
, it deletes the given bookmark. - When I send a GET request to
/
, it loads the index.html file of my client. - When I send a GET request to
/*
, it looks for the corresponding file of my client public folder.
File tree
The file tree for the server contains three elements: the client
folder (described in the previous part), package.json
, the manifest for the application and server.js
, the code for the server.
client/
package.json
server.js
Manifest
Here is the manifest for the whole application:
{
"name": "my-bookmarks",
"version": "1.0.3",
"description": "Example of single-page application that you can easily deploy and share",
"main": "server.js",
"scripts": {
"build": "cd client && npm run-script build"
},
"repository": {
"type": "git",
"url": "https://github.com/frankrousseau/shareable-app"
},
"author": "Cozy Cloud",
"license": "MIT",
"dependencies": {
"body-parser": "1.12.2", // Turn JSON Body into Javascript
"express": "4.12.3", // To build our REST API server
"express-error-handler": "1.0.1", // To handle error properly
"method-override": "2.3.2", // To manage HTTP methods properly
"morgan": "1.5.2", // For proper logging
"path-extra": "1.0.2", // To manage local files easily
"pouchdb": "3.3.1", // The database
"slug": "0.8.0" // To make machine-friendly strings
}
}
The build
command is here to ensure that the client is properly built. The other important thing is the dependencies
field which allow you to grab all the required dependencies. They will be saved in the node_modules
folder after your run the following command:
npm install
Express
Now let's start building our server. First, we load all our dependencies.
var path = require('path');
var slug = require('slug');
var express = require('express')
var morgan = require('morgan');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var PouchDB = require('pouchdb');
Then we create the Express server. It will allow us to define our REST API.
var app = express();
User story covered: When I send a GET request to /
, it loads the index.html
file of my client.
User story covered: When I send a GET request to /*
, it looks for the corresponding file of my client.
Then, we configure it. The next line says that our client/public/
folder will be served at the root url. For instance, it means it will serve the client/public/index.html
when the user connects to http://localhost:9125/index.html
.
app.use(express.static(path.join(__dirname, 'client', 'public')));
Then we configure Express to behave properly as REST API that deals only with JSON.
app.use(methodOverride());
app.use(bodyParser.json());
The last configuration thing will be to configure the logger to write in the console when a request is handled by the server.
app.use(morgan('dev'));
The next step is to define a basic controller that will manage all GET requests targeting the /api
path. That controller will answer with a simple JSON object.
With Express, a controller is a function that takes a request object and a response object in argument. The request object will give every informations given by the request such as the headers, the body and the parameters. The response object will help to build a proper responses. To end its process, the controller must use the send
method of the response. That action will send the built response to the client.
Let's write our first controller. It sends a welcome message at JSON format:
var controllers = {
base: {
index: function (req, res) {
res.send(welcome: 'My Bookmarks API v1.0');
}
},
}
app.get('/api', controllers.base.index);
Our server is now properly configured. To be started, it requires two basic parameters: port on which the server will listen and host IP on which server will be binded (allowed to be accessed from). To make it brief, it says that you can connect to it via this URL: http//host:port
. '0.0.0.0'
means that any IP is accecepted. For security reason, it's best to put '127.0.0.1'
.
var port = process.env.PORT || 9125;
var host = process.env.HOST || '0.0.0.0';
var server = app.listen(port, host, function () {
console.log('Example app listening at http://%s:%s', host, port)
});
PouchDB
Our server is up and running but is almost useless. We want to make it able to handle bookmark persistence. To do that we will need a database. No need to install an external one like MySQL or MongoDB. We'll use an embedded database called PouchDB. Let's intantiate it:
var PouchDB = require('pouchdb');
var db = PouchDB('db');
User story covered: When I send a GET request to /bookmarks
, it loads the bookmark list at JSON format.
Then we'll define three new controllers. The first one will allow us to get all our bookmarks. It will retrieve the bookmarks stored in the database.
Pouch has a query language based on map/reduce, a mechanism to filter and build the results from the whole database. For our case, it only requires one map function, a function that will tell to only get doc with type field equal to bookmarks. Yes, Pouch stores just a big list of documents. That's why to get our docs, we need to filter them all.
Then, we put all our results in a array. This array is pushed to the client via the response object.
bookmarks: {
all: function (req, res) {
var allBookmarks = function (doc) {
if (doc.type === 'bookmark') {
emit(doc._id, null);
};
};
db.query(allBookmarks, {include_docs: true}, function (err, data) {
if (err) {
console.log(err);
res.status(500).send({msg: err});
} else {
var result = [];
data.rows.forEach(function (row) {
result.rows.push(row.doc);
});
res.send(result);
}
});
},
User story covered: When I send a POST request to /bookmarks
with required fields at JSON format, it creates and persist the bookmark.
Creating a bookmark is simpler. We first grab information for the body. We ensure that the information are properly formatted. Then, we build an id for this bookmark by slugifying the link field.
We check that no similar bookmark is already stored by ensuring that no bookmark exist with this ID. If there is no bookmark, we can create it and send the created bookmark as result.
create: function (req, res) {
var bookmark = req.body;
if (bookmark === undefined || bookmark.link === undefined) {
res.status(400).send({msg: 'Bookmark malformed.'});
} else {
var id = slug(bookmark.link);
db.get(id, function (err, doc) {
if (err && !(err.status === 404)) {
console.log(err);
res.status(500).send({msg: err});
} else if (doc !== undefined) {
res.status(400).send({msg: 'Bookmark already exists.'});
} else {
bookmark.type = 'bookmark';
bookmark._id = id;
db.put(bookmark, function (err, bookmark) {
if (err) {
console.log(err);
res.status(500).send({msg: err});
} else {
res.send(bookmark);
}
});
}
});
}
},
User story covered: When I send a DELETE request to /bookmarks/:id
, it deletes the given bookmark.
Here is a particularity. We need to extract a parameter from the URL, it's the id of the bookmark to delete. Express did the job for us, we can grab it from the params field of the req instance.
With the id we check if the bookmark already exists. If it succeeds, we remove the document from the database.
Note that we set status code before sending our response. They are usual HTTP code that says if an error occured (500), information is missing (404) or if the operation succeeded (20x).
delete: function (req, res) {
var id = req.params.id;
db.get(id, function (err, doc) {
if (err) {
console.log(err);
res.status(500).send({msg: err});
} else if (doc === null) {
res.status(404).send({msg: 'Bookmark does not exist.'});
} else {
db.remove(doc, function (err) {
if (err) {
console.log(err);
res.status(500).send({msg: err});
} else {
res.sendStatus(204);
};
});
}
});
}
}
};
Then we register every methode/route/controller triples to Express. The HTTP method is represented by a function of the same name. The route is the first parameter and the controller the second parameter of the function call.
app.get('/api/bookmarks', controllers.bookmarks.all);
app.post('/api/bookmarks', controllers.bookmarks.create);
app.delete('/api/bookmarks/:id', controllers.bookmarks.delete);
Our server is now fully configured and ready to act as a REST API for bookmark management!
Conclusion
NB: Read the full server code to have a good overview of what we did.
We're done! We have a fully functional bookmark manager. You can now start your server and connect to http://localhost:9125. Your client should work and interact with the REST API properly. It means that you can refresh the page and that your last change will be saved.
Now, let the magic happens. In the next and last part of the tutorial, we'll speak about how to publish our brand new app on NPM and allow others to run it with two command lines.