How I made a simple JSON API in Node.js

Sam Walpole
9 min readAug 15, 2018

--

Recently I’ve been working through the Advanced Web Developer course by Colt Steele on Udemy. We’ve been working through how to make RESTful JSON API, so I thought I’d put my new-found skills to the test by creating my own JSON API.

A running production version can be found here. All the code for the final version can be found here.

The Challenge

Coming from a chemistry background, my idea was create an API that returned the mass of any compound that was passed to it as a parameter.

For example, if I submitted a HTTP GET request to the route, /api/weight/H2O,I would expect to get back a mass of 18 (hydrogen has a mass of 1, and oxygen has a mass of 16). In the final version, this is returned as JSON,{"H2O":18}.

I also decided to create a second route that would split a formula into its constituent elements and count how many we had of each. For example, a request to api/parse/CH3CH2OH would return the following JSON, {"C":2,"H":6,"O":1}.

Finally I made a simple front-end to consume the API and provide a more user-friendly experience.

Dependencies

This app uses the Express framework and embedded Javascript templates.I would also recommend Nodemon for during development.

Code Organisation

I’ll try to be brief here, but I think it helps to understand the way that I’ve rationalised my file structure.

In the root directory, I simply have the main app file (app.js) and a file called periodic.js, which contains an object with the masses of every single element.

There is a /routes directory, which contains all of the, yes you guessed it, routes…

In order to keep the /routes directory clean, I have moved all of the associated functions to files in a helpers directory.

The /views directory contains all of the front-end template files. For this project, I used embedded JavaScript templates (ejs). There is also a views/partials subdirectory that contains the header and footer, which is used on every frontend page.

Finally, the public directory contained my custom CSS file (I was mostly utilising Bootstrap).

Root Files

/app.js

const express = require('express');
const app = express();
const port = process.env.PORT;

Set up the app to use the Express framework. All future calls to app use methods from this framework. Next I set the port that I would later listen from. I was developing in Cloud9 and this is the way they expose their port, but it may well be different in other environments.

app.set('view engine', 'ejs');

I mentioned that I was using ejs templates to structure my front-end. These allow data to be passed to them dynamically, but at runtime the variables need to be replaced with actual values. This tells Express that it needs to interpret the templates as ejs files.

app.use(express.static(__dirname + '/views'));
app.use(express.static(__dirname + '/public'));

Allows the static files in the /views and /public directory to be served by the server.

app.use('/', require('./routes'));

Include all of the routes defined within the route folder.

app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});

Tell the app to listen on the specified port. The app is now running.

As I mentioned before, periodic.js simply exports an object containing the weight of all the elements with the format {"element":weight}. I won’t list it here because it has well over 100 keys! But you get the point.

Routes Files

/routes/index.js

This file manages all of the frontend routes, and requires /routes/api.js, which contains the API endpoint routes.

const express = require('express');
const router = express.Router();

The express.Router() class allows routes to be set up in a modular fashion. i. e. across multiple JavaScript files. This is instead of having all of the routes defined in app.js, which would get very messy very quickly!

router.get('/', (req, res) => {
res.render('index', {page: 'home'});
});
router.get('/api', (req, res) => {
res.render('api', {page: 'api'});
});
router.get('/contact', (req, res) => {
res.render('contact', {page: 'contact'});
});

These are the three routes used by the frontend, /, /api, and /contact. The router.get method is used to respond to HTTP GET requests made to the path specified in the first parameter.

The second parameter is a function to call when a request is made to that route. Here I am using arrow functions, which are new to ES6. If you’ve not seen them before, check them out here. The callback is passed two objects which contain request (req) and response (res) data and methods. In each case, I am calling the method res.render, which tells the app to render a particular view (remember earlier we specified that our view engine should use ejs). The first parameter is the name of the file we wish to render. In app.js we already told in to look for views in the /views folder, and it knows the file extension should be .ejs, so we can omit these. The second parameter allows you to pass data to the ejs template that can be used to dynamically change the frontend. The data is passed as an object, and, in this case, I have a key called page that I am changing to match the name of the route. I will use this later in my header.ejs file to dynamically update tho navigation bar.

router.use('/api', require('./api'));module.exports = router;

The first line is used to import the routes from /routes/api.js. The first parameter here is used to add the /api prefix to each route specified in api.js. We’ll see this in action below.

Finally, the second line exports the router object as a Node module, so that it can be imported into the main app.js file.

/routes/api.js

This file handles all the routes related to the API endpoints. Here we have just two, /parse and /weight, both of which are HTTP GET requests.

const express = require('express');
const router = express.Router();
const helpers = require('../helpers');
router.get('/parse/:mol', helpers.parseMol);
router.get('/weight/:mol', helpers.sumWeight);
module.exports = router;

Since the methods used to parse the input and calculate the molecular weight are a little lengthy, I have moved them to a separate file, which is imported here onto the helpers object.

It’s important to note that in the /routes/index.js file we imported the api.js file with the prefix /api. Therefore the full route paths here are actually api/parse/:mol and /api/weight/:mol.

But what does :mol actually mean? Anything with a : preceding it is known as a route parameter. Firstly, it means that these routes will match any value for :mol. For example, both /api/parse/XYZ123 and /api/parse/C2H5Cl are both valid routes here. Secondly, the app takes these parameters and saves them as a variable that I can use later. In this case, I would have a variable called mol that would contain 'XYZ123' or 'C2H5Cl' respectively. We will be using this later in our helper functions.

Helpers Files

/helpers/index.js

const periodic = require('../periodic');

Firstly we need to require the periodic object, since we are using these helpers methods to calculate the molecular weight of the input.

const split = str => {
const i = str.search(/\d+/);
if(i === -1) return [str, 1];
const ele = str.slice(0, i);
const amount = parseInt(str.slice(i), 10);
return [ele, amount];
};

This first method, split, expects a string as an input, corresponding to a single element and the number of those elements. For example, 'C3', 'H5', and 'Cl' are all valid inputs.

First it searches the string for the starting index of a number consisting of one or more digits. If there are no numbers, then it assumes there is only one of those elements and returns an array containing the element name and 1 (e.g. ['Cl', 1]).

Otherwise, it puts everything before the number in a variable called ele and puts the number into a variable called amount. Finally it returns an array with the format, [ele, amount].

const parseMol = input => {
const matches = input.match(/[A-Z][a-z]?\d*/g);
if(matches === null) return {error: 'Input could not be parsed. Check input'};
const parsed = {};
matches.forEach(match => {
if(!(split(match)[0] in periodic)) {
parsed.error = `Element '${split(match)[0]}' not recognised`;
} else {
if(parsed[split(match)[0]]) parsed[split(match)[0]] += split(match)[1];
else parsed[split(match)[0]] = split(match)[1];
}
});
return parsed;
};

The method, parseMol, takes the input as given by a user (e.g. 'C2H5Cl') and parses it into a JavaScript object (e.g. {"C": 2, "H": 5, "Cl": 1}).

It starts by splitting the input string into an array by matching it to the regular expression, /[A-Z][a-z]?\d*/g. This expression requires a capital letter, followed by an optional lowercase letter, followed by zero or more digits. If no matches are found, an object is returned containing an error property.

If matches are found, we loop through each match. First we test to see if the element within the match is actually in our periodic database. If not, we add an error property to the parsed object. Note that here we are utilising the split method that we wrote above.

Otherwise, we add the amount of elements in the match to a property with that elements name. The reason for the if-statement here is so that if the property already exists, we add the new amount to the existing amount; otherwise we create a new property. This is so, if a user enters the input, 'C2C1', this method will return {"C": 3}, instead of overwriting it.

By now we have the API set up and working. If you run the app and submit a request to one of the two routes, you should now get JSON back. Now to work on the frontend!

Views Files

I won’t bore you with all of the details of every view file, since most of it is just HTML and should be fairly self explanatory. But I’ll point out some of the more interesting features.

/views/partials/header.ejs

Firstly I have included references to import Bootstrap (a CSS library for quick styling), Font Awesome (an icon library for my social media icons), and jQuery (a JavaScript library to make manipulating the DOM easier).

The interesting part of this file comes in the nav menu, where we use ejs to dynamically update our anchor tags:

<a class="nav-link <% if(page === 'home') { %> active <% } %> " href="/">Home</a>

Here we have a conditional statement set up with ejs so that if the variable, page, is equal to home then we add the class, active, to the anchor tag. Using Bootstrap, this has the effect of filling in that navigation pill. If you remember, we sent this page variable to the ejs template in the /routes/index.js file, and we changed the content of the variable depending on which route the user requested. Therefore, we are able to dynamically update the active navigation pill depending on which page we are on!

/views/index.ejs

One of my motivations for using ejs templates over simple HTML files was the ability to include files. This both makes the code cleaner and lets you reuse certain elements, like the header, over multiple pages, without having to rewrite it every time. Includes in ejs look like this:

<% include ./partials/header %>

The final thing to point out in the /views/index.ejs file are the various elements with ids that we can later access with jQuery:

<input type='text' name='input' id='input' class='form-control my-2' placeholder='Enter chemical formula. E.g. C2H5Cl' />
<button id='submitInput' class='btn btn-primary'>Calculate</button>
<button id='helpButton' class='btn btn-info'>Help!</button>
<p id='output' class='lead my-5'></p>
<div id='help' class='text-left' hidden>...

In particular, we have an empty paragraph, to which the result of our JSON API query will be loaded to later.

Also, note that the div with id, help, has the attribute hidden. This is so that the help text is invisible when the page loads. We will then use jQuery to toggle this attribute on and off when the help button is clicked.

Public Files

/public/index.js

This file contains two methods, getWeight, which sends a GET request to the api/weight route of the API, and toggle, which toggles the hidden attribute on a particular element.

const getWeight = input => {
$.get(`/api/weight/${input}`)
.then(res => {
let str;
if(res.error) str = res.error;
else{
str = 'The molecular weight of the compound, ' +
Object.keys(res)[0] +
', is: ' +
res[input] +
' g/mol.';
}
$('#output').text(str);
})
.catch(err => console.log(err));
};

To start with, the jQuery get method sends a GET request to the /api/weight with the user’s input. This method returns a promise, which, if rejected, calls the catch method to log the error to the console. If the promise is resolved, the then method is called.

Remember, the /api/weight route returns the parsed input as an object containing its molecular weight. If there were any problems, then the error is added to the result string, str. Else, a string is constructed containing the key and the value of the returned object. Finally, the content of the string is rendered to the div with id, output.

const toggle = selector => {
if($(selector).attr('hidden')) {
$(selector).removeAttr('hidden');
} else {
$(selector).attr('hidden',true);
}
};

This is a simple if-statement that tests if the given selector has the hidden attribute. If it does, then that attribute is removed, if it doesn’t then it is added.

$(document).ready(() => {
$('#input').keypress(event => {
if(event.which == 13) getWeight($('#input').val());
});

$('#submitInput').click(() => {
getWeight($('#input').val());
});

$('#helpButton').click(() => {
toggle('#help');
});
});

Finally, we set up three event handlers to use our newly-made methods. The first sets up a keypress handler on the input with id, input. If the key keycode is 13 (this is the enter key), then the value in the input is sent the /api/weight route using getWeight.

Similarly, if the button with id, submitInput, is pressed, then again the value of the input is sent to the API.

Finally, we add a click handler to the button with id, helpButton such that, when it is clicked, the toggle method is called on the div with id, help.

The front-end should now be fully functional and this project is finished!

Again, the production version can be found here, and the source files can be found here.

Thanks for coding along! If you have any comments or questions, please feel free to contact me. If I could have done anything better, I’d love to hear your suggestions.

--

--