Since its inception in 2009, Node.js has grown leaps and bounds in popularity. In parallel, the community has also contributed libraries and third-party packages. By virtue of its ability to run server-side logic, Node.js enjoys a lot of adoption for implementing REST APIs.
In this blog post, we show you how to build a REST API using Node.js.
This post is a two-part series. This first part covers the essential considerations for a developer to build a REST API and show a demo REST API built using Node.js natively. That is, using the built-in modules of Node.js, you will be able to host a fully functional REST API service. In the second part, we will show you how to use the third-party frameworks in the Node.js ecosystem to speed up REST API development for production deployment.
What is REST API?
REST or REpresentational State Transfer is a set of guidelines for building web services interfaces. Since APIs are the most prevalent form of interface used in the World Wide Web, this concept is associated with APIs and referred to as REST API. It is also known as RESTful API.
The REST guidelines define a set of rules and best practices for designing APIs. It was initially proposed by Roy Fielding, the author of HTTP specification based on his Ph.D. dissertation “Architectural Styles and the Design of Network-based Software Architectures”. While the actual REST guidelines specify quite a few rules and constraints covering architecture and consistency of APIs, the crux of developing a good RESTful API boils down to these three tenets:
- Stateless: A REST API assumes that the client handles all the state information related to the application. The server does not maintain any state information across API invocations. Thus, each client request contains enough information to represent the state that gets transferred to the server as part of the client-server request initiation.
- Unique Resource Identification: A resource is an application-specific entity that maps to a well-described form. For example, in a logistics management system, the shipment is a resource that identifies a package in transit. Therefore the REST API for this system should uniquely identify each shipment using a unique URL resource path.
- Resource Manipulation: The client maintains the resource representation and invokes the server using the HTTP methods to manipulate the resource. For example, every shipment goes through the standard CRUD operation to create, read, update, and to delete the shipment from the logistics system. A good REST API uses the unique URL identifier and the HTTP method to perform CRUD operations.
How To Design a REST API?
By following the above tenets, it is possible to build a good REST API for any SaaS-based application that mimics a real-world operation.
To understand this concept, let’s consider the logistics application in more detail. An operator using this application would be tracking shipments. Therefore, the shipment becomes a primary resource around which REST APIs are built.
In this way, the application server platform of the logistics application will define the following APIs.
POST | /api/logistics/shipment | Create a new shipment |
GET | /api/logistics/shipment/{shipment_id} | Retrieve shipment |
PUT | /api/logistics/shipment/{shipment_id} | Update shipment |
DELETE | /api/logistics/shipment/{shipment_id} | Delete shipment |
GET | /api/logistics/shipment/search | List shipments |
As you can see, each API has a URL that identifies either the operation performed on the shipment or the shipment itself. The {shipment_id} is the placeholder for the actual shipment id, which is used within the URL for all shipment specific APIs. The client application used by the logistics operator will invoke these API endpoints to perform the day to day logistics activities tracked by the system.
Similarly, think of any other SaaS use case that needs to expose APIs for the client apps. You can infer the primary resources used in the use case and design the APIs around that. Here are a few examples that you can relate with:
- E-Commerce: In e-commerce applications, the primary resources that are tracked are the products and orders. Each product will have a product id. Therefore, a set of CRUD APIs manages the product listings in the e-commerce portal. Similarly, customer orders are tracked via order ids. Hence, another group of CRUD APIs works the orders.
- HRMS: In an HR management system, employees are the primary resource. The REST APIs exposed by this application are modeled around CRUD operations on employees based on when a new employee joins, or an employee leaves, or gets promoted.
- Cyber Security: In cybersecurity systems, some assets are monitored for suspicious activities. As an example, the system might be tracking a set of IPs. In this case, a REST API is designed around managing IP addresses and their whereabouts.
Developing the REST API Interface
Developing a REST API involves writing the backend logic for handling API requests. This process is similar to a software development cycle, where you start with a high-level design and then implement the code at a low level. Along the way, there are design considerations around choosing the most efficient algorithm, third-party tools and libraries, databases, and more.
In its simplest form, the API implementation consists of API internals and API backend.
API internals is the business logic for servicing the API request. API backend consists of several backend components, such as databases and third-party libraries/services.
Our Demo REST API Interface
Let’s walk through one of the API design scenarios that we addressed in the last section and see how it fits this API implementation model.
Consider the cybersecurity use case for IP monitoring. This IP monitoring service’s objective is to track a set of IP addresses for their assigned geolocation. The REST APIs exposed by this service are modeled around managing IP addresses as the primary resource.
Therefore we can define the REST API Interfaces for the IP monitoring service as follows:
POST | /api/ipmon/ip | Add a new IP address for tracking |
GET | /api/ipmon/ip/{ip_address} | Retrieve IP address details |
PUT | /api/ipmon/ip/{ip_address} | Update IP address details |
DELETE | /api/ipmon/ip/{ip_address} | Delete IP address and stop tracking it |
GET | /api/ipmon/ip/show | List the currently tracked IP addresses |
Deciding on the API Backend Components
What about the database and third-party services.
For every IP address being tracked by the system, there should be a database to store its details. This is an important decision. However, we will think about it a little later when we jump into coding in the following section.
As for getting the details of the IP address’s geolocation, this API must rely on a third-party service. This is because IP addresses are assigned and managed by a global organizational entity, and it is best to use an external service to retrieve that information from them.
How to get over this problem? Use another API at the backend to retrieve the IP address details for you.
Choosing a Third-party API for Retrieving IP Address Geolocation
RapidAPI’s API marketplace offers a rich collection of APIs suited for different applications. From industry verticals such as finance, travel, and healthcare, to technology domains such as data analytics, visual recognition, and cloud communication, RapidAPI hosts thousands of APIs from different API providers to suit your needs.
For our demo REST API interface, we can go with the IP Geolocation API. This API is simple to use. It is a single endpoint that returns geo location-related information for a public IP address.
Before leveraging this API, you have to signup for a free RapidAPI account and subscribe to this API. Follow along with the steps below.
Step 1. Sign Up for RapidAPI Account
Sign up for a free RapidAPI developer account. With this account, you get a universal API Key to access all APIs hosted in RapidAPI’s API marketplace.
RapidAPI is the world’s largest API marketplace, with over 10,000 APIs and a community of over 1,000,000 developers. Our goal is to help developers find and connect to APIs to help them build amazing apps.
Step 2. Subscribe to IP Geolocation API
Once signed in, log in to your RapidAPI account and access the API console.
Click on the “Pricing” tab and opt-in for the basic subscription. IP Geolocation API offers a generous quota of 10k API hits per month.
Step 3. Test Your API Subscription
Let’s test the IP Geolocation API subscription.
Head back to the “Endpoints” tab in the API console, and select the “GET JSON endpoint” endpoint.
Key in a valid public IP address for the ‘ip’ field under optional parameters and trigger the API. You should see an API response on the “Results” panel on the API console’s right side.
Here is a sample response from the IP Geolocation API.
The API returns quite a few parameters identifying ISP’s region’s geographical and political parameters that owns the IP address. Based on the IP address selected by you, the response will differ.
With this step, you have successfully subscribed to the IP Geolocation API and verified its functionality. Now you are ready to use this API within the REST API service that we have envisaged for IP address monitoring.
Implementing the IPMon REST API Interface with Node.js
Let’s christen this demo API as IPMon API. With the IPMon API interface finalized earlier and the backend API chosen through RapidAPI, you are now ready to take the plunge into coding.
Before you start writing the code, make sure that you have the right programming environment to develop and test the API locally.
Prerequisites
- Node.js Runtime: Download and install Node.js from https://nodejs.org/en/. Recommended to download and install the LTS version ( currently 12.18.2 LTS). You can refer to https://docs.npmjs.com/downloading-and-installing-node-js-and-npm for the steps to install Node.js along with npm (node package manager)
- cURL: Download and install the curl command link utility from https://curl.haxx.se/download.html. This tool will be used for testing the API interface.
Development Steps
Launch your favorite code editor and open a command-line terminal. Follow the steps below to complete the development for the IPMon API service.
Step 1: Create a Node.js project
It is a good practice to create a top-level project directory. On the terminal, run the following command to create the top-level directory for this project under your chosen path.
mkdir node-api |
node-api is your top-level directory, which will have all the project-specific files. Change to this directory and create a new Node.js project.
cd node-api npm init -y |
The npm init command will initialize an empty project, and generate a file named ‘package.json’. We are not following the Node.js toolchain for this application, so you can ignore this file’s contents.
Create a new file named ‘server.js’.
touch server.js |
Step 2: Add the HTTP API Endpoints
The ‘server.js’ file is the main source code for the REST API interface.
First, you have to add the module imports that are used in this code.
const http = require ('http'); // to create server const url = require('url'); // to parse url const https = require('https');// to send https requests var ip_table = new Map();// for simplicity using has map to store ip details instead of db
This API interface relies on the Node.js http module for handling REST APIs. The url module is used to parse URL, and https is used for triggering external API.
Apart from that, this code also initialized ip_table, which is a JavaScript Map, that holds key-value pairs. This object serves as an in-memory database for storing all the IP addresses being monitored by the API.
This is not the best way of implementing a database for real-world applications. However, it suffices for this post since our focus is to get the API interface up and running quickly.
Next, you have to create the HTTP server endpoints for handling the IPMon API requests.
// create basic server and implement handling different requests const app = http.createServer( async (req, res) => { // parse the incoming url const parsedURL = url.parse(req.url, true); //check for POST /api/ipmon/ip if ( parsedURL.pathname === '/api/ipmon/ip' && req.method === 'POST') { // check if ip is prsent in query else send error if(!parsedURL.query.ip) { res.statusCode = 400; res.end("IP address is required, please add IP address in the query"); } else { handleCreate(parsedURL.query.ip,res); } } //check for GET /api/ipmon/ip/show else if (parsedURL.pathname === '/api/ipmon/ip/show' && req.method === 'GET'){ handleShow(res); } //check for PUT /api/ipmon/ip/{ip_address} else if (parsedURL.pathname.startsWith('/api/ipmon/ip') && req.method === 'PUT'){ var ipAddr = extractIPAddress(parsedURL.pathname) if(!ipAddr.length) { res.statusCode = 400; res.end("Invalid IP Address"); } else { handleUpdate(ipAddr,res); } } //check for DELETE /api/ipmon/ip/{ip_address} else if (parsedURL.pathname.startsWith('/api/ipmon/ip') && req.method === 'DELETE'){ var ipAddr = extractIPAddress(parsedURL.pathname) if(!ipAddr.length) { res.statusCode = 400; res.end("Invalid IP Address"); } else { handleDelete(ipAddr,res); } } //check for GET /api/ipmon/ip/{ip_address} else if (parsedURL.pathname.startsWith('/api/ipmon/ip') && req.method === 'GET'){ var ipAddr = extractIPAddress(parsedURL.pathname) if(!ipAddr.length) { res.statusCode = 400; res.end("Invalid IP Address"); } else { handleRead(ipAddr,res); } } // if url doent match any send error else { res.statusCode = 400; res.end("API Endpoint Not Supported"); } });//End of create server. app.listen(4000);
This code sets up a Node.js app to listen for requests on port 4000. It defines the individual endpoints for the IPMon API interface, with the allowed HTTP methods. API endpoints are declared the same way as we specified them earlier. Each endpoint calls a handler function.
- POST /api/ipmon/ip – handleCreate( )
- GET /api/ipmon/ip/show – handleShow( )
- PUT /api/ipmon/ip/ – handleUpdate( )
- DELETE /api/ipmon/ip/ – handleDelete( )
- GET /api/ipmon/ip – handleRead( )
Step 3: Add the API Endpoint Handlers
The hander functions implement all the API business logic. Let’s add the handlers one by one and understand their internal code.
Append the ‘server.js’ file with the function handleCreate( ) for handling the /api/ipmon/ip request.
/* function to handle create */ function handleCreate( ip,res) { // call geolocation api and get the details getGeolocation( ip ).then( response => { // update the database - map in this case updateTable(response); // 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(JSON.stringify(ip_table.get(ip))); }, error => { res.statusCode = 400; res.end(error); } ) }//End of handle create
When the client application triggers this endpoint, it requests the IPMon service to start tracking the geolocation of a new IP address.
Since the IPMon API relies on the IP Geolocation API as a backend service for fetching the details of the IP addresses’ geolocation, this handler invokes the IP Geolocation API. It stores the result in the Map object.
This handler invokes two internal functions, getGeolocation( ) and updateTable( ). You will define them in the subsequent step.
Next up, append the ‘server.js’ file with the function handleShow( ). This handler is for /api/ipmon/ip/show
function handleShow(res){ // set the header and status res.setHeader('content-type', 'Application/json'); res.statusCode = 200; // create an array from the map array = Array.from(ip_table, ([name, value]) => ({ name, value })); // send the response array res.end(JSON.stringify(array)); }
This code is straightforward. The client requests the IPMon API to show all the IP addresses being tracked. Therefore, the API retrieves all the key-value pairs stored in the Map objects and returns it as a string.
Next, you have to add the handleRead( ) function.
// function to handle read request function handleRead(ip,res){ // check if the ip is present in table if(ip_table.has(ip)){ // set header, status code and send the entry res.setHeader('content-type', 'Application/json'); res.statusCode = 200; res.end(JSON.stringify(ip_table.get(ip))); } else { // ip not found send error res.statusCode = 404; res.end("Read: IP "+ip+" not found"); } }
This function queries the IP address passed as part of the API request and returns its data from the Map object.
Now append the handleDelete( ) function to ‘server.js’ as follows.
// function to handle delete request function handleDelete(ip,res){ // check if ip is in the table if(ip_table.has(ip)){ // delete and send the success response ip_table.delete(ip); res.statusCode = 200; res.end("Delete: "+ip+" deleted"); } else { // ip not found send error res.statusCode = 404; res.end("Delete: IP "+ip+" not found"); } }
This handler also queries for the IP address passed as part of the API request and deletes it from the Map.
Finally, append the handleUpdate( ) function.
// function to handle update request function handleUpdate(ip,res){ // check if the ip is present in table if(ip_table.has(ip)){ // update the details by calling geoplocation api getGeolocation( ip ).then( response => { // set header, status code, update table and send response res.setHeader('content-type', 'Application/json'); res.statusCode = 200; updateTable(response); res.end(JSON.stringify(ip_table.get(ip))); }, error => { // send error res.statusCode = 400; res.end(error); } ); } else { // ip not found in table so send error res.statusCode = 404; res.end("Update: IP "+ip+" not tracked") } }
This handler is similar to the handleCreate( ).
Update happens wherever the client application wants to check the current geolocation status of an existing IP address. Before returning the API response, this API also calls the getGeolocation( ) function and updates the Map object with the API response from IP Geolocation API. This ensures that the update operation on an IP address returns the latest geolocation.
Step 4: Add the internal functions
There are two internal functions used by few handlers, getGeolocation( ) & updateTable( ).
Append the ‘server.js file to add these functions. First you have to add getGeolocation( ).
const rapidAPIBaseUrl = "https://rapidapi.p.rapidapi.com/json/?ip="; 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
This function handles the interactions with the IP Geolocation API. It returns a Promise object that resolves to the API response data in case of success.
After copying this function code into your ‘server.js’ file, make sure that the placeholder <YOUR_RAPIDAPI_KEY> is replaced with your RapidAPI subscription key.
Moving ahead, append the ‘server.js file to add updateTable( ).
function updateTable(entry){ // 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 ip_table.set(entry.ip, {"ip":entry.ip,"country":entry.country,"city":entry.city,"lastUpdated":time}); }
This function is responsible for writing to the Map object. For every IP address, it stores the country and city name. These parameters are retrieved from the IP Geolocation API response. It also adds an internally generated timestamp to record the last updated time for the IP address record.
Apart from this, there is one more internal function, extractIPAddress( ).
This function is used within the main createServer calls for validating and extracting the IP addresses from the API endpoints. Add this function to the ‘server.js’ file.
function extractIPAddress(path){ var ipAddress = path.substring(path.lastIndexOf("/")+1,path.length); if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ipAddress)) { return ipAddress; } else { console.log("invalid IP Address") return ""; } } // End of extractIPAddress
This function extracts the IP address based on the last ‘/’ character from the URL path. It then applies a regular expression match to check the validity of the IP address.
With this step, you have completed coding for the IPMon API interface. Make sure that you save the file.
Step 5: Run the server
You can run the ‘server.js’ file using the Node runtime.
node server.js |
If all goes well without any errors, then this program will launch the IPMon service as an API hosted on localhost URL http://localhost:4000
Testing the IPMon API Interface
Testing this API is pretty easy since we already defined the API interface earlier.
Fire up a new terminal and run the following cURL command.
curl -X POST http://localhost:4000/api/ipmon/ip?ip=64.23.14.10 |
C:\>curl -X POST http://localhost:4000/api/ipmon/ip?ip=64.23.14.10 {“ip”:”64.23.14.10″,”country”:”United States”,”city”:”Chicago”,”lastUpdated”:”2020-11-03T18:08:44.265Z”} |
This will add the IP address 64.23.14.10 in the database for tracking, along with its geolocation data.
Now trigger the show endpoint to check if the IP address is there.
curl -X GET http://localhost:4000/api/ipmon/ip/show |
C:\>curl -X GET http://localhost:4000/api/ipmon/ip/show [{“name”:”64.23.14.10″,”value”:{“ip”:”64.23.14.10″,”country”:”United States”,”city”:”Chicago”,”lastUpdated”:”2020-11-03T18:08:44.265Z”}}] |
You can also get the specific IP by querying the IP address in the API.
curl -X GET http://localhost:4000/api/ipmon/ip/64.23.14.10 |
C:\>curl -X GET http://localhost:4000/api/ipmon/ip/64.23.14.10 {“ip”:”64.23.14.10″,”country”:”United States”,”city”:”Chicago”,”lastUpdated”:”2020-11-03T18:08:44.265Z”} |
Similarly you can update the IP address.
curl -X PUT http://localhost:4000/api/ipmon/ip/64.23.14.10 |
C:\>curl -X PUT http://localhost:4000/api/ipmon/ip/64.23.14.10 {“ip”:”64.23.14.10″,”country”:”United States”,”city”:”Chicago”,”lastUpdated”:”2020-11-03T18:16:10.168Z”} |
And finally, you can delete the IP address.
curl -X DELETE http://localhost:4000/api/ipmon/ip/64.23.14.10 |
C:\>curl -X DELETE http://localhost:4000/api/ipmon/ip/64.23.14.10 Delete: 64.23.14.10 deleted |
Since the IP address is now deleted, any subsequent attempt to directly query that IP address will return an error from API.
C:\>curl -X GET http://localhost:4000/api/ipmon/ip/64.23.14.10 Read: IP 64.23.14.10 not found |
Voila!
Now you have a working REST API with Node.js.
In the second part of this post, where we will implement this same IPMon service using Node.js and Express.js. We will also bring in an actual database component in the form of MongoDb to add some real-life flavor to building APIs.
Stay tuned!
Leave a Reply