Test Category

Test Blog Post

Starter template for writing out a blog post using MDX/JSX and Next.js.

No Name Exists

Abdullah Muhammad

Published on May 17, 20265 min read 3 views

Share:
Article Cover Image

Introduction

Depending on the functionality of the web application, you may or may not need to incorporate external data into your application.

Whether it is from a database or through an API call, testing to see how your application works can be expansive.

First, you would have to design your application, build out that database or API, and finally test it through client-side after having built out that side of the application as well.

Thankfully, there are tools out there which make life easy when it comes to testing.

Instead of building out an entire front-end for testing, we can bypass all this using testing tools. One of the most popular tools out there for API testing is Postman.

Today, we will dive deep into using this tool to test custom built APIs using Node.js without a front-end.

What is Postman?

Postman is an API platform which developers can use. It is more than simply a testing tool although that is what it is mostly known for. It allows for the collaboration, creation, and publication of APIs both public and private.

A central hub for thousands of APIs, integrated Git support, and so much more. There are pricing models for different features Postman offers ranging from basic to enterprise.

For more details, you can visit the official site here. We will focus on using the testing tool Postman has to offer.

Note: For the tutorial demo, you WILL need the Postman software installed locally.


Postman Testing Tool Interface

Once installed and opened, you should see an interface that looks like this:

No Image Found
Postman Testing Interface with a clean UI allowing for different options for API testing

On the left side, you have a history panel which details past API requests made at their particular dates. At the top, you have the option to sign in to your Postman account as well as work under workspaces which you can create.

In the centre, is where most of the action takes place. You have the ability to determine request type, request URL, and numerous other options such as headers, request body, authorization, parameters, and so much more.

At the bottom, lies the response panel where response data and errors will be visible along with any other notifications and response status codes.


Postman Interface Visual Descriptions

The following screens are features of Postman highlighted in a red box with a short description:

No Image Found
All HTTP request types supported
No Image Found
Parameters for API requests using simple key-value pairs supported
No Image Found
Authorization using different options such as API keys, JWT, OAuth supported
No Image Found
Headers for content-type, connection type, and additional features supported
No Image Found
Sending requests with a body enabled using different formats such as raw and form-data supported
No Image Found
Settings for the enabling and disabling of SSL certificate verification, redirects, and headers supported

APIs and HTTP Requests/Responses in Detail

API stands for Application Programming Interface. It allows for the communication of applications between computers offering information.

Often times, you can think of this interaction as the client-server communication which we have looked at many times before.


HTTP Request Types

There are different HTTP request types and they follow the CRUD pattern we looked at in databases:

  • GET — Request type for fetching of data (Read)
  • POST — Request type for inserting data, but it could be anything (Create)
  • PUT — Request type for updating the whole set (Update)
  • PATCH — Request type for updating part of data (Update)
  • DELETE — Request type for the deletion of data (Delete)

GET and POST tend to be the most commonly used request types you will encounter, but it is important to note and remember the lesser common ones.


HTTP Request URL Parameters

You can also have dynamic values passed into certain URLs known as parameters. These can be informative for helping the server further understand how to process a given request.

An example can be the following:

GET https://www.example.com/api/{id}

Here, only a GET request is acceptable and the {id} portion of the URL is a parameter accepting dynamic values. This means that anything can be passed in as a value and it will be interpreted as the id.


HTTP Request Body Data Types

When working with requests types such as PUT, PATCH, and POST you might be wondering what options you have for sending data.

The most common format used for data interchange is JSON, but you can use HTML, XML, and Plain Text.


HTTP Request Headers

Headers in requests add context to the request and help the server figure out how to best process it. Common ones you might be familiar with are content-type, connection, host, user-agent, and accept.

For content-type, formats such as application/json, text/plain, text/html, multipart/form-data are applicable and commonly used.

We can also have authorization headers passed into a request.


HTTP Request Authorization Headers

When trying to access resources that require authorization, you will need to pass in sensitive data to validate the request.

Different types of Authorization include:

  • API keys
  • Jsonwebtoken
  • Certificates
  • Basic Auth
  • Bearer Tokens
  • OAuth

And so much more…

You can use Authorization as the header and pass in any of the following above or use a custom defined name such as X-API-KEY. Nonetheless, you have a host of options for working with authorization.

When we worked with JWTs and Protected Routes, we used an Authorization header to pass in a JWT token and have it processed in a middleware function before proceeding to the actual request.


HTTP Response Status Codes

When sending requests, you would like some feedback to tell you about the status of your request.

HTTP response codes do just that and these are categorized in one of the following five groups:

  • Informational Responses (100–199) — HTTP request is processed, but the user is notified of certain details pertaining to their request as a response.
  • Successful Responses (200–299) — HTTP request was successful. Some codes you might be familiar with are 200 OK and 201 Created.
  • Redirection Responses (300–399) — HTTP request is handled, but it is either redirected to another URI or a new URL is given as a response.
  • Client Error Responses (400–499) — HTTP request was not successful and the client (user) is at fault. Some codes you might be familiar with are 400 Bad Request, 401 UnAuthorized, 403 Forbidden, and of course, 404 Not Found.
  • Server Error Responses (500–599) — HTTP request was not successful and it is the server at fault. The user requests are correct, but the server has an issue handling it. Some codes you might be familiar with are 500 Internal Server Error and 502 Bad Gateway.

We will use status codes in server responses to client requests.

For more details on HTTP status codes, you can refer to the official docs here.

Code Overview

You can follow along by cloning the following repository.

The directory we will work with is /demos/Demo17_Postman_API_Testing. Since we are working with Postman for API testing, there is no front-end to this application.

We have built a back-end server using Node.js and Express allowing one request for each of the five different request types (GET, POST, PUT, PATCH, and DELETE).

There is no database either, but we have cleverly used a JSON file to act as a mock database containing user related data.

So in essence, we will be testing CRUD operations against this file by sending requests via Postman to the back-end server.

There are certain rules that must be followed when trying to test each of these five endpoints.

They are outlined in the README.md file /backend/README.md. You can think of this as API documentation. The following screen detail those rules:

No Image Found
README.md file containing rules for working with each of the five different requests

Here is the routes file containing one route for each of the five different requests outlined in the README.md file above /backend/Route/APIRoute.js:

GitHub GistJavaScript
const express = require("express");
const APIController = require("../Controller/APIController");

const router = express.Router(); // Router for working with different paths

// Routes to be added later, focusing on the 5 types: GET/POST/PUT/PATCH/DELETE
router.get("/fetch-data", APIController.fetchData);
router.post("/insert-data", APIController.insertData);
router.put("/update-whole-data", APIController.updateWholeData);
router.patch("/update-partially-data", APIController.updatePartiallyData);
router.delete("/delete-data", APIController.deleteData);

module.exports = router;
Route file defining five different routes one for each of the five different request types

The following is the mock database containing data to start with /backend/data.json:

GitHub GistJSON
{
    "data": [
        {
            "userId": 1,
            "title": "Teacher"
        },
        {
            "userId": 2,
            "title": "Newbie"
        },
        {
            "userId": 3,
            "title": "Developer"
        }
    ]
}
data.json file containing a data array consisting of three different user objects

When reading or writing data to this file, we will need the exact path to this file. Stored in the path.js file, is a constant containing this value using the Node.js built-in path module /backend/path.js:

GitHub GistJavaScript
const path = require("path");

// Exporting data.json file path for usage
exports.dataFilePath = path.join(__dirname, 'data.json');
path.js file containing exact location to the data.json file for reading and writing data

And finally, where most of the heavy lifting is done, the controller file containing five functions for handling each of the five different request types /backend/Controller/APIController.js:

GitHub GistJavaScript
// Controllers for handling each of the routes to be added here
const fs = require("fs");
const filePath = require("../path");

// GET Method Implementation
exports.fetchData = (req, res) => {
    // Read data.json file and provide the path of the file from a separate module
    fs.readFile(filePath.dataFilePath, (err, data) => {
        if (err) {
            res.status(400).json({
                message: "Could not read data"
            });
        }
        else {
            // File data should be parsed for readability
            res.status(200).json({
                APIDATA: JSON.parse(data).data
            });
        }
    });
}

// POST Method Implementation
exports.insertData = (req, res) => {
    let { data } = req.body;

    // Read file data
    fs.readFile(filePath.dataFilePath, (err, fileData) => {
        if (err){
            res.status(400).json({
                message: "Could not read file"
            })
        }
        else {
            // Extract the array storing file data
            let parsedFileData = JSON.parse(fileData).data;

            // Filter request dataset based on ID, duplicates not allowed
            for (var i = 0; i < parsedFileData.length; i++) {
                data = data.filter(x => x.userId !== parsedFileData[i].userId);
            }

            // For each entry to be entered, push each user object
            for (var j = 0; j < data.length; j++) {
                parsedFileData.push(data[j]); 
            }
            
            // Write data object to file, after pushing it programmatically and converting stream to string
            fs.writeFile(filePath.dataFilePath, JSON.stringify({ data: parsedFileData }), err => {
                if (err) {
                    res.status(400).json({
                        message: "Could not write to file"
                    });
                }
                else {
                    res.status(200).json({
                        message: "Successfully written data to file"
                    });
                }
            });
        }
    });
}

// PUT Method Implementation
exports.updateWholeData = (req, res) => {
    const { data } = req.body;

    // Write data object to file anew. Removing all the old content and replacing with data object
    fs.writeFile(filePath.dataFilePath, JSON.stringify({ data }), err => {
        if (err) {
            res.status(400).json({
                message: "Could not write to file"
            });      
        }   
        else {
            res.status(200).json({
                message: "Successfully written data to file"
            });
        }
    });
}

// PATCH Method Implementation
exports.updatePartiallyData = (req, res) => {
    let { data } = req.body;

    // Read file data
    fs.readFile(filePath.dataFilePath, (err, fileData) => {
        if (err){
            res.status(400).json({
                message: "Could not read file"
            });
        }
        else {
            // Extract the array storing file data
            let parsedFileData = JSON.parse(fileData).data;

            // For each entry to be update, update their title
            for (var j = 0; j < parsedFileData.length; j++) {
                for (var k = 0; k < data.length; k++) {
                    if (data[k].userId === parsedFileData[j].userId){
                        parsedFileData[j].title = data[k].title;
                    }
                    else {
                        continue;
                    }
                }
            }
            
            // Write data object to file, after pushing it programmatically and converting stream to string
            fs.writeFile(filePath.dataFilePath, JSON.stringify({ data: parsedFileData }), err => {
                if (err) {
                    res.status(400).json({
                        message: "Could not write to file"
                    });
                }
                else {
                    res.status(200).json({
                        message: "Successfully written data to file"
                    });
                }
            });
        }
    });

}

// DELETE Method Implementation
exports.deleteData = (req, res) => {
    let { data } = req.body;

    if (data.length > 0) {
        // Read file data
        fs.readFile(filePath.dataFilePath, (err, fileData) => {
            if (err){
                res.status(400).json({
                    message: "Could not read file"
                })
            }
            else {
                // Extract the array storing file data
                let parsedFileData = JSON.parse(fileData).data;

                // Filter request dataset based on IDs that exist
                for (var i = 0; i < data.length; i++) {
                    parsedFileData = parsedFileData.filter(x => x.userId !== data[i].userId);
                }
                
                // Update data file to consist of entries not requested to be deleted
                fs.writeFile(filePath.dataFilePath, JSON.stringify({ data: parsedFileData }), err => {
                    if (err) {
                        res.status(400).json({
                            message: "Could not write to file"
                        });
                    }
                    else {
                        res.status(200).json({
                            message: "Successfully written data to file"
                        });
                    }
                });
            }
        });
    }
    else {
        // If length of data object is empty, remove all entries
        fs.writeFile(filePath.dataFilePath, JSON.stringify({ data: [] }), err => {
            if (err) {
                res.status(400).json({
                    message: "Could not write to file"
                });
            }
            else {
                res.status(200).json({
                    message: "Successfully written data to file"
                });
            }
        });
    }
}
APIController.js file containing implementation for each of the five different request types

We will briefly touch on these five functions:

  • GET fetchData() — Enables client to fetch all user related data currently stored in the data.json file.
  • POST insertData() — Enables client to pass request body containing user information to insert into the data.json file. We use the built-in function filter() and pass in a predicate function to prevent duplicates from being stored.
  • PUT updateWholeData() — Enables client to pass request body containing user information to update the entire data.json file.
  • PATCH updatePartiallyData() — Enables client to pass request body containing user information to update some user information.
  • DELETE deleteData() — Enables client to pass request body containing information on users to delete and if an empty array is passed, the entire dataset is deleted. We use the built-in function filter() and pass in a predicate function to remove requested users.

To expedite development, we made use of the readFile() and writeFile() functions from the built-in Node.js fs module to help with the reading and writing of the data.json file.

The built-in JSON functions stringify() and parse() also came handy as we were dealing with strings and data manipulation.

Demo Time!

Now that we have completed the code overview, it is time for the demo!

Assuming you have cloned the repository from above, ensure you have Postman installed locally on your machine.

There are two steps you will need to complete before proceeding.

  1. Navigate to /demos/Demo17_Postman_API_Testing/backend as your current working directory and run npm install to install all the required packages for this project.
  2. Run node server to kickstart the Node server. It should be running on port 5000.

You should be notified that you are Listening to PORT 5000 on the console and if so, you are good to go.

We can begin to test the first method GET /fetch-data using Postman. Following the rules outlined in the README.md file, you should have sent a request like this and received the following response:

No Image Found
Successful GET /fetch-data request with response containing data from the data.json file

From this call, we can see a status code of 200 OK and a response containing the same data currently stored in the data.json file.

Now let us test the POST /insert-data method using Postman. Use the following data locally and you should be notified of a successful request:

No Image Found
Successful POST /insert-data request with response containing message and status code 200 OK

Purposefully, the request data contains two duplicate user ids (1, 2 already exist inside data.json) and one new user id.

However, as we will see by fetching data again, that only the new user id was inserted and the duplicates were not:

No Image Found
Successful GET /fetch-data request containing user data with only the new entry stored in the data.json file

Now let us test the PUT /update-whole-data method using Postman. Use the following data locally and you should be notified of a successful request:

No Image Found
Successful PUT /update-whole-data request with response containing message and status code 200 OK

This should have updated the entire dataset inside the data.json file and only these two entries should exist.

That is indeed the case if we fetch the data again:

No Image Found
Successful GET /fetch-data request containing the updated user data in its entirety

Now let us test the PATCH /update-partially-data method using Postman. Use the following data locally and you should be notified of a successful request:

No Image Found
Successful PATCH /update-partially-data request with response containing message and status code 200 OK

Purposefully, the request data contains an entry that does not exist in the data.json file. That entry being user id 6.

We will see that by fetching data again, that only the current existing user ids in the data.json file have their values updated:

No Image Found
Successful GET /fetch-data request containing updated user data specific to the existing users

Now let us test the DELETE /delete-data method using Postman. Use the following data locally and you should be notified of a successful request:

No Image Found
Successful DELETE /delete-data request with response containing message and status code 200 OK

The data.json file should only contain one user entry as user id 5 is now deleted.

If we fetch data again, that is indeed the case:

No Image Found
Successful GET /fetch-data request containing only one user entry with user id 5 now deleted

And finally, now let us test the DELETE /delete-data method again, but this time, pass in an empty array. This should delete all entries inside the data.json file:

No Image Found
Successful DELETE /delete-data request with response containing message and status code 200 OK

If we fetch data again, we should receive an empty array from the data.json file:

No Image Found
Successful GET /fetch-data request containing no entries from the data.json file

That is all for the demo! We successfully tested each method using mock data and Postman and found out that they all work as intended.

If you want to test this application using data of your own, make sure to follow the rules outlined in the README.md file as noted earlier.

Conclusion

That is it for this tutorial. We did a deep dive into Postman and learned many of the key features their testing tool has to offer. We saw how we could develop and test a back-end server without needing a client interface.

Dove deep into APIs and HTTP request types, response codes, request body data types, parameters, and most importantly authorization.

We also found a clever way to test without a database using a simple JSON file containing mock data.

We also looked at a README.md file which roughly illustrated how API documentation works in the real world.

In the list below, you will find links to the GitHub repository and the official Postman site:

I hope you enjoyed this article and look forward to more in the future.

Thank you!

No Name

Abdullah Muhammad

Blogger. Software Engineer. Designer.

Subscribe to the newsletter

Get new articles, code samples, and project updates delivered straight to your inbox.