In part one of this post, we covered the nuances of designing and building a REST API. We also covered the basic implementation of a demo REST API for the IPMon service with Node.js.
In this post, we will enhance the IPMon implementation by adding a few more components to it. These additions will make it more robust for real-world deployment.
Deploying Consideration for Node.js REST API in Production
A REST API mimics a real-world application and defines interfaces for manipulating resources. We have covered these concepts in part one and demonstrated with the IPMon REST API, where IP addresses were the primary resources tracked via the API.
As we witnessed, building and launching REST APIs with Node.js is easy. However, deploying them for a production environment is another challenge. That’s when the rubber meets the road, and a few things can go wrong.
That’s why you need a few more components to make your REST API worthy of real-world deployment.
Web Framework
The IPMon REST API was implemented using the native Node.js libraries for handling the HTTP protocol. However, for writing more readable and maintainable code, you need a web framework.
A web framework provides boilerplate components that make it easy to implement request and response handling routines for REST APIs. It also includes utility modules for common tasks such as parsing, response formatting, error handling, and many more.
Database
We used a Map object in the IPMon REST API to store the IP address and their details. Map is an in-memory variable, and it does not support data persistence.
For all practical purposes, an in-memory variable cannot handle the volume of data needed to be stored. Databases are the tried, tested and de-facto components used for storing data in any web application. They provide persistence of data, efficient searches as well as guarding against faults.
Infrastructure
A barebone Node.js application cannot scale and handle sudden spikes in traffic. Scalability is a beast in itself, and it requires additional architectural considerations to expand the system, both horizontally and vertically, to handle the demand.
Most of it is defined as an additional layer of infrastructure components such as load balancers or container orchestrators. These are outside the purview of a Node.js application.
Choosing The Right Components for IPMon REST API
Additional components should be considered to upgrade the IPMon REST API closer to a real-world implementation.
Express
Express is one of the most popular web frameworks supported by the Node.js ecosystem. It is fast, lightweight, and is easy to learn for an average JavaScript programmer. That’s why it is also used as a base framework for many other specialized frameworks.
MongoDB
Choosing the right database for your REST API is a separate discussion in itself. But given the trends, MongoDB is one of the popular databases used by web applications. It is quick to set up and supports document storage and retrieval, which is ideal for unstructured data.
Implementing the IPMon Service with Express and MongoDB
With these new components, we will attempt to build the next iteration of the IPMon REST API.
To recap, here is the enhanced architecture of the app with additional components.
Architecture
The API endpoints remain the same as per the earlier IPMon service we designed in part 1. The standard Node.js implementation is now replaced with the Express based JavaScript implementation and a connection to the MongoDB database.
Prerequisites
The prerequisites for building this API also remain the same as the previous implementation without Express. However, instead of the in-memory Map, you will need to set up a MongoDB database.
To make it simple, you can create a free account at MongoDB Atlas and deploy a basic database instance.
Follow the Getting Started Guide to deploy a free tier cluster of MongoDB on AWS.
Also, ensure that as part of the Connect to Your Cluster step, you choose Node.js as the driver and install the MongoDB package using npm.
At the end, you will get a custom connection string to connect to your MongoDB Atlas instance. Make sure to keep a note of this string.
Development Steps
Now you are ready to start coding.
Follow the steps below to add the code for implementing this new version of IPMon REST API.
Step 1: Initialize the project
Create a directory named ‘node-api’. This serves as the top-level project folder.
Change to the directory and initialize a new Node.js project with the default configuration.
npm init -y |
This will create a package.json file, but you can ignore it for a simple project like this one.
Create a file named ‘server.js’.
touch server.js |
Since we will build this as an Express application, you have to install the library as follows:
npm install express –save |
This command will install the express library locally for this project.
Step 2: Initialize the source file
All the code for this project will be contained within the ‘server.js’ file. Open this file on a text editor and add the following code to import all the required modules.
const express = require('express') const app = express(); const url = require('url'); // to parse url const https = require('https');// to send https requests const mongoClient = require('mongodb').MongoClient // initialize geolocation api base url const rapidAPIBaseUrl = "https://rapidapi.p.rapidapi.com/json/?ip="; // create basic server and implement handling different requests app.listen(4000,function(){ initialize(); console.log("listening on 4000"); })
|
This code sets up the Express server to listen on port 4000.
Step 3 : Add API endpoints
Append the file with a new function named initialize( ).
function initialize(){ const uri = "<YOUR_MONGODB_CONNECTION_STRING>"; const client = new mongoClient(uri, { useNewUrlParser: true ,useUnifiedTopology: true }); client.connect(err => { if (err) { console.log("error"); console.log(err); client.close(); } else { console.log("connected to db "); const geoCollection = client.db("geo").collection("geolocation"); app.post('/api/ipmon/ip', function(req,res){ console.log("in POST /api/ipmon/ip"); const parsedURL = url.parse(req.url, true); handleCreate(req.query.ip,res,geoCollection); }); app.get('/api/ipmon/ip/show',function(req,res){ console.log("in GET /api/ipmon/ip/show"); handleShow(res,geoCollection); }); app.get('/api/ipmon/ip/:ipa',function(req,res){ console.log("in GET /api/ipmon/ip/"); const parsedURL = url.parse(req.url, true); handleRead(req.params.ipa,res,geoCollection); }); app.put('/api/ipmon/ip/:ipa', function(req,res){ console.log("in PUT /api/ipmon/ip/"); const parsedURL = url.parse(req.url, true); handleUpdate(req.params.ipa,res,geoCollection); }); app.delete('/api/ipmon/ip/:ipa', function(req,res){ console.log("in DELETE /api/ipmon/ip/"); const parsedURL = url.parse(req.url, true); handleDelete(req.params.ipa,res,geoCollection); }); } }) }
|
This function performs two operations.
It initializes the MongoDB client with the connection allocated by MongoDB Atlas.
After that, it defines the API endpoints as per the HTTP methods by calling the Express methods. Each method defines the API endpoint and calls a handler function.
Ensure that you replace the placeholder <YOUR_MONGODB_CONNECTION_STRING> with the actual string you got from your free tied cluster setup.
Step 4 : Add API handler functions
Append the ‘server.js’ file with these functions that define the API handlers called from within the initialize( ) function above.
/* function to handle create */ function handleCreate( ip,res, geoCollection) { // call geolocation api and get the details getGeolocation( ip ).then( response => { if(response.success){ insertRecord(response,geoCollection); // set the header and status code success and return the details of the ip res.setHeader('content-type', 'Application/json'); res.statusCode = 200; res.end("record created : " + ip); } else { res.statusCode = 400; res.end(response.message); } }, error => { res.statusCode = 400; res.end(error); } ) } /* function to handle show*/ function handleShow(res,db){ //db.collection('geolocation').find({},{projection:{_id:0}}).toArray() db.find({},{projection:{_id:0}}).toArray() .then(results => { // set the header and status res.setHeader('content-type', 'Application/json'); res.statusCode = 200; // create an array from the map res.send(JSON.stringify(results)); }) .catch(error => console.error(error)) } /* function to handle update */ function handleUpdate( ipAddress,res, db) { // call geolocation api and get the details var query = { ip: ipAddress }; db.find(query,{projection:{_id:0}}).toArray() .then(results => { if(results.length >0){ getGeolocation( ipAddress ).then( response => { updateRecord(response,db); // set the header and status code success and return the details of the ip res.setHeader('content-type', 'Application/json'); res.statusCode = 200; var time = new Date(); var respJson = {"ip":response.ip,"country":response.country,"city":response.city,"lastUpdated":time} res.end("record updated : "+ JSON.stringify(respJson)); }, error => { res.statusCode = 400; res.end(error); } ) } else { // ip not found send error res.statusCode = 400; res.send("Read : "+ipAddress+" not found"); } }); } /* function to handle read request */ function handleRead(ipAddress,res,db){ // check if the ip is present in table var query = { ip: ipAddress }; db.find(query,{projection:{_id:0}}).toArray() .then(results => { if(results.length >0){ // set header, status code and send the entry console.log("results:"); console.log(results); res.setHeader('content-type', 'Application/json'); res.statusCode = 200; res.send(JSON.stringify(results)); } else { // ip not found send error res.statusCode = 400; res.send("Read : "+ipAddress+" not found"); } }) } /* function to handle delete */ function handleDelete(ipAddress,res,db){ // check if ip is in the table var query = { ip: ipAddress }; db.deleteOne(query, function(err, obj) { if (err) { res.statusCode = 400; res.send("delete : "+ipAddress+" not found"); } // n in results indicates the number of records deleted if(obj.result.n == 0){ res.statusCode = 400; res.send("delete : "+ipAddress+" not found"); } else { res.statusCode = 200; res.send("record deleted : " + ipAddress); } }); }
|
There are five functions to handle the requests to the five API endpoints of IPMon REST API.
handleCreate( ) and handleUpdate( ) call a few more internal functions for handling IP geolocation query and MongoDB operations.
handleShow( ) and handleRead( ) directly operate through the MongoDB collection, using the find( ) function to list all IP addresses or get a specific IP address, respectively.
The handleDelete( ) function calls the deleteOne( ) function on MongoDB to delete an IP address record.
Step 5 : Add API helper functions
To support the handleCreate( ) and handleUpdate( ) function there are three internal functions. Let’s add them to the file now.
function getGeolocation( ipAddress ) { // initilize http.rquest object var req = https.request; // initialize header with the required information to call geolocation api. var header = { "x-rapidapi-host": "ip-geolocation-ipwhois-io.p.rapidapi.com", "x-rapidapi-key": "<YOUR_RAPIDAPI_KEY>", "useQueryString": true }; // add the query string including the IP address var query_string = { "ip" : ipAddress }; // set the options parameter var options = { headers: header, query: query_string }; // form the url const url = rapidAPIBaseUrl + ipAddress ; return new Promise ( ( resolve, reject) => { https.get( url, options, res => { let data = ""; //data is received in chunks, so uppend data as and when received res.on( 'data', function(response) { data = data + response; }); // handle error if any res.on( 'error', function(err) { console.log("Error"); console.log(err); }) // if end of data return the final chunk res.on( 'end', () => { resolve( JSON.parse(data) ); }); });//Endn of http }); //end of return promise }//End of getGeolocation function insertRecord(entry,geoCollection){ // get current date to update last update the time var time = new Date(); // add the entry to table ip is the key and country, city and last updated time are stored data = {"ip":entry.ip,"country":entry.country,"city":entry.city,"lastUpdated":time} geoCollection.insertOne(data) .then((result,error) => { if(error){ console.log(error); } }).catch(error => console.error("error")) } function updateRecord(entry,geoCollection){ // get current date to update last updated time var time = new Date(); // add the entry to table ip is the key and country, city and last updated time are stored var query = {"ip":entry.ip} data = { $set : {"ip":entry.ip,"country":entry.country,"city":entry.city,"lastUpdated":time}} geoCollection.updateOne(query,data) .then((result,error) => { }).catch(error => console.error("error")) }
|
The getGeolocation( ) function is used to call the IP GeoLocation API to retrieve the location details for each IP address that is added to the IPMon service. You have to replace the placeholder <YOUR_RAPIDAPI_KEY> with your RapidAPI subscription key.
The insertRecord( ) function inserts a new IP address record to MongoDB using the insertOne( ) call on the MongoDB client.
The updateRecord( ) function updates an existing IP address record to MongoDB using the updateOne( ) call on the MongoDB client.
With these functions, the code for the IPMon REST API service is complete. Save the ‘server.js’ file.
Testing the IPMon Service
The testing procedure for this API remains precisely as before.
Run the ‘server.js’ file, and using the cURL utility, verify all the API endpoints.
Wrapping Up
Now you have a functional REST API with Express.js. With MongoDB, your API has a real database at the backend that persists all the data across server outages.
With this implementation, you are a step closer to production-grade API deployment. As the next step, you have to deploy it on a server that is publicly accessible through an IP address or a domain name.
You also have to worry about scalability to handle a surge in traffic. For that, you need to think of strategies to deploy your API with load balancers or use platforms such as Kubernetes. This is a separate set of topics that require different expertise so let’s not delve further.
We hope you now have a fair idea about what it takes to build a robust REST API with the Node.js ecosystem. For any queries, please leave your comments below.
Leave a Reply