Many developers use JavaScript to add client-side functionality to their web pages. JavaScript can be used to create server-side web pages and web APIs too. In this three-part article series, you're introduced to Node.js and Express and you'll learn their roles in the creation of a web server. You're going to build a new web server from scratch, create some routes to return data, and learn how to modularize your web application. You'll be guided step-by-step to creating a set of APIs to return an array of objects or a single object, search for a set of objects, and add, edit, and delete objects. If you've never used Node.js or Express to build a server, don't worry: this article has everything you need to learn the basics of working with these two powerful tools.

Over the next three articles, you'll learn to handle errors in Express using middleware and log errors to a file. You'll learn to use some additional npm packages to retrieve settings from a configuration file, and to read and write data to and from SQL Server. You're going to learn to build a website using Node.js and Express and make calls to your Web APIs from the pages on this website. You'll learn to configure CORS to allow cross-domain resource sharing. Using Express and Mustache, you're going to see how to build dynamic web pages with the data retrieved from your Web API calls.

Node.js Explained

Node.js is a web server like IIS or Apache, but is cross-platform and open source. Node.js can run on Windows, Linux, Unix, macOS, and many other operating systems. Node.js is a JavaScript runtime environment that runs on Google's V8 JavaScript engine. This means that you get all the good (and bad) of the JavaScript language for programming back-end tasks.

Node.js lets developers use JavaScript to write any type of web server environment they want. Using Node.js, you can serve up static HTML files, dynamic web pages, and Web API calls. One of the best things about using Node.js is that you use JavaScript for all your back-end programming, and on your web pages, you use JavaScript to manipulate your web pages and/or make Web API calls back to your JavaScript web server. Instead of using C#, Visual Basic, or Java for your back-end coding and JavaScript for your front-end coding, you use a single language for both.

Node.js is an event-driven architecture that makes it great for throughput and scalability. Node.js runs as a single-threaded, non-blocking, asynchronous programming paradigm, which is very memory efficient. These features make it a fast and scalable platform for web development.

The Express Framework

Although you can build web applications using Node.js and JavaScript, sometimes the tasks to build a simple web server can take many lines of code. Many frameworks to make this task simpler have been developed around the Node.js infrastructure. One such framework is called Express and is probably the most used. For complete documentation on Express, check out https://expressjs.com. Express is a framework to help you create websites and Web APIs with less code and easier-to-use features than Node.js by itself. You're free to use all the features of Node.js as well.

Why Use Express?

Express is a thin wrapper around many Node.js features and adds new features as well. Some things that Express adds that make creating web applications so easy are middleware, routing, and templating. If you've used Microsoft ASP.NET, you're probably already familiar with these concepts. Middleware is a small piece of code you write that allows you to have access to incoming requests and outgoing responses. This access allows you to look at each request and modify the response that's sent back out. Middleware is often used for exception handling as well as other generic tasks.

Routing is how a web application responds to client requests and calls the appropriate code to perform a task. For example, if you allow a user to call your Web API using http://www.example.com/api/product, this call is routed by Express to a specific JavaScript function that might return a product web page, a simple string, or it might send an array of JSON product objects back to the caller.

Express also supports the ability to build dynamic web pages using templating engines. Templating helps you build dynamic web pages by marrying data and HTML to create a final HTML page to send to the user. In ASP.NET, you might use the Razor syntax combined with C# objects to create a list of products that the user views as an HTML page on their browser. In Express, you can use many different templating engines to generate this same list of products on an HTML page. Express allows you to use the templating engine of your choice such as EJS, Pug, and Mustache.

Set Up Your Development Environment

If you've never used Node.js or Express before, I highly recommend you follow along with the steps in this article to gain proficiency with these tools. You're going to need a few tools (Table 1) on your computer to create the Web application in this article.

Of the tools listed in Table 1, Postman and Visual Studio Code are optional. Instead of Postman, you can test retrieving data from your Web API calls using a browser. There are other tools, such as SoapUI and Fiddler, that have similar functionality to Postman. Instead of Visual Studio Code, any editor can be used for developing your JavaScript files. You're free to use Visual Studio 2022 if you're familiar with that. I'm using the tools listed in Table 1 for this article because I'm used to them, and because they are probably the most used tools for developing Web APIs with Node.js.

Install Node and Node Package Manager (npm)

The first step is to see if you have Node.js and the Node Package Manager (npm) on your computer. Open a Command Prompt or open the Windows PowerShell app on your Windows computer and type in the following to see if you have node installed.

node -v

You'll either see an error, or you'll see a version number appear for node. If you have Node.js installed, you most likely have npm installed, as these two are generally downloaded and installed as one package from the https://nodejs.org website. If Node.js isn't installed on your computer, go to https://nodejs.org and download the LTS version (not the Current version). If you have a Node.js version older than version 12.x, please download the latest version to upgrade your computer.

Install Visual Studio Code

If you already use Visual Studio Code, or you already have your editor of choice, you can skip to the next section. If you wish to install Visual Studio Code, navigate to http://code.visualstudio.com in your browser and download and install Visual Studio Code. If you're using a different editor, you might have a different way to start the Web API project. Wherever in this article I say to use the terminal window to submit a command, you should use whatever is appropriate for your editor.

Install Postman

If you already have a preferred method of testing your Web APIs, skip to the next section of this article. If you wish to give Postman a try for testing your Web APIs, navigate to https://www.postman.com/pricing in your browser and click the Sign Up button under the Free version of Postman. Download and install Postman on your computer.

Create a Node.js Project Using VS Code

Open a Command Prompt, the Windows PowerShell app, or some other terminal as appropriate for your computer, and navigate to where you normally place your development projects. For this article, I'm using the folder D:\Samples to create my new Node.js project. After opening a command prompt within your development folder, create a new folder under that folder and navigate to that folder using the following two commands.

mkdir AdvWorksAPI
cd AdvWorksAPI

Open the Visual Studio Code editor in this new folder using the following command. Note, that this is the word code, followed by a space, followed by a period (.).

code .

From the menu system in VS Code, open a new terminal by selecting Terminal > New Terminal. Type in the following command in the terminal window to start building a new JavaScript project.

npm init

The terminal window asks you for some information to describe this project. If you wish to accept the default answers, simply press the Enter key after each prompt, otherwise enter the appropriate information for your project, as shown in Figure 1. At the end, answer yes to save a file called package.json in your new project folder. The package.json file contains meta-data about your project to help npm run the scripts in the project, install dependencies, and identify the initial JavaScript file used to start the application.

Figure 1: Answer a series of questions to create a package.json file for your project.
Figure 1: Answer a series of questions to create a package.json file for your project.

Install Express and Nodemon

You're now ready to install any additional utilities you want to use in your application. You're going to use the Express framework for creating the web server, so let's install that now. From within the terminal window, install the Express framework using the following command within the terminal window.

npm install express

You're also going to use the nodemon utility to automatically detect changes to any files in your project and restart your web server when appropriate. Install nodemon by using the following command within the terminal window.

npm install nodemon

Modify the package.json File

Open the package.json file and you should now see Express and nodemon listed under the Dependencies property. Also notice that there's a main property that lists index.js as its value. This is the starting JavaScript file for the application. Because you want to use nodemon to watch for any changes in your js files, add a Start property under the Scripts property, as shown in the code snippet below.

"scripts": {
  "start": "nodemon index.js",
  "test": "echo \"Error: no test specified\" && exit 1"
},

Be sure to save the changes to the package.json file at this point.

Create Express Server and Get Data

Now that the configuration and main files of the web application have been created, add a new file named index.js to the root folder of your project. The index.js file is the starting point for web applications. This file is analogous to the Program.cs file in .NET applications. Into the index.js file, add the code shown in Listing 1.

Listing 1: Create an index.js file as the starting point for your application.

// Load the express module
const express = require('express');
// Create an instance of express
const app = express();
// Specify the port to use for this server
const port = 3000;

// GET Route
app.get('/', (req, res, next) => {
  res.send("10 Speed Bicycle");
});

// Create web server to listen on the specified port
let server = app.listen(port, function () {
  console.log(`AdvWorksAPI server is running on http://localhost: ${port}.`);
});

A Word About Modules

The code shown in Listing 1 uses the built-in Node.js function called require() to load the express module. A module is a JavaScript file that defines some functionality to be called by your application. Where are these modules located? When you performed the npm init command, a folder named node_modules was created with the Node.js server JavaScript files and all related dependencies. When you performed the npm install express command, a folder named express was created under the node_modules folder. All the JavaScript files needed for the express framework were downloaded into this folder. The require() function looks into the node_modules folder and locates the folder with the name you specify in the require() function. From there, it figures out where the module named Express is located and instantiates that object.

You can think of modules (very loosely) as a class in C#. A module splits the functionality of an application into separate files. A great thing about Node.js modules is that the scope of the module is just within the one file; they're not in a global memory space, as is the case with JavaScript running in the browser.

Getting Data with Express

Immediately after loading the Express module, call the express() function to create an application object. The application object is used to route incoming HTTP requests, configure middleware, and set up the listening for requests on a specific port. Next, you see the app.get(...) that's used to map a request coming in to the JavaScript code within the app.get() function. For example, if a GET request comes in on http://localhost:3000, the code within the app.Get("/" ...) function is run. The code res.send(“10 Speed Bicycle”) returns the string 10 Speed Bicycle to the caller. The last lines of this index.js file start the server running on port 3000 on your local computer.

Try It Out

Save the changes you made to all the files in your project. In your terminal window, type in the following command to start the Node.js server and use nodemon to run the JavaScript code in your index.js file.

npm start

After typing in this command, you should see the message AdvWorksAPI server is running on http://localhost:3000. This is the message you specified within the app.listen() call. Open your browser and submit the request http://localhost:3000 via the URL line to view the string returned from this Web API call. You should see the data appear in your browser, as shown in Figure 2.

Figure 2: Call the Web API from your browser to have the data returned.
Figure 2: Call the Web API from your browser to have the data returned.

Using Postman

If you have Postman installed, open Postman (Figure 3) and in the URL line (#1), type in http://localhost:3000. Click the Send button (#2) to submit the call to your running Web API application and you should see the string 10 Speed Bicycle appear in the body window (#3). Congratulations! You have just created your first Web API call using Node.js and Express.

Figure 3: Use Postman to send Web API requests and to view the data returned.
Figure 3: Use Postman to send Web API requests and to view the data returned.

Return an Array of Product Objects

Instead of just returning a single string from a Web API call, let's add an array of product objects to return. Open the index.js file and modify the app.get() function you created as your route to look like Listing 2.

Listing 2: Return an array of product objects from your Web API.

app.get('/', (req, res) => {
  // Create array of product objects
  let products = [{
      "productID": 879,
      "name": "All-Purpose Bike Stand"
    },{
      "productID": 712,
      "name": "AWC Logo Cap"
    },{
      "productID": 877,
      "name": "Bike Wash - Dissolver"
    },{
      "productID": 843,
      "name": "Cable Lock"
    },{
      "productID": 952,
      "name": "Chain"
    }
  ];
  res.send(products);
});

Try It Out

Save the changes you made the index.js file. From within your browser, or from within Postman, submit the same request, http://localhost:3000. You should see the array of product objects appear. The one nice thing about using Postman is you also see the status codes appear after each call, as shown in Figure 4, right with the call. When using your browser, you need to go into the F12 developer tools to see the status code.

Figure       4: Postman shows the status code along with the data returned from the API call.
Figure 4: Postman shows the status code along with the data returned from the API call.

Return a Status Code and JSON

By default, all successful API calls return a status code of 200 along with the data. You may change the status code that's returned if you wish. Open the index.js file and modify the app.get() function to return a status code of 206 using the code shown in the following snippet:

app.get('/', (req, res, next) => {
  // Create array of product objects
  let products = [
    // PRODUCT ARRAY HERE
  ];

  res.status(206);
  res.send(products);
});

Instead of using send() to return the data, you can also use the json() function using the code res.json(products). When passing JSON objects or arrays, both send() and json() are equivalent. When a simple string value is passed to send(), it sets the content-type HTTP header to text/html whereas the json() function sets the content-type HTTP header to application/json. The json() function takes the input and converts it to a JSON object, then performs a JSON.stringify() on that object before calling send(). Feel free to use whichever function you want, as your needs dictate. You're allowed to chain the status() and json(), or status() and send() functions together instead of having two separate lines of code, as shown in the following code snippet.

res.status(206).json(products);

If you want, add the status(206) call to your previous code and resubmit your query to see the new status code returned along with the array of product objects.

Send JSON Object Instead of Just the Data

If you've ever consumed an API call from JavaScript in a browser, you know that you generally don't just get the data back, you get a JSON object containing the status code, the status text, maybe a message, and then the data. You should follow the same conventions when developing your Web API return values. Open the index.js file and modify the app.get() function to look like the following code snippet.

app.get('/', (req, res, next) => {
  // Create array of product objects
  let products = [
    // PRODUCT ARRAY HERE
  ];

  res.json({
    "status": 200,
    "statusText": "OK",
    "message": "All products retrieved.",
    "data": products
  });
});

Try It Out

Save the changes you made to the index.js file. Resubmit the request, http://localhost:3000, to your web server. You should see the JSON object with the status, statusText, message, and data properties appear. If you're using Postman, notice that it formats the data returned very nicely, as shown in Figure 5. Unfortunately, when you submit the Web API call through a browser, the JSON object returned is simply dumped unceremoniously onto the page with no formatting. This is another reason I prefer to use Postman instead of a browser for testing API calls.

Figure       5: A nice feature of Postman is that it formats the JSON object that's returned.
Figure 5: A nice feature of Postman is that it formats the JSON object that's returned.

Create a Module for Your Product Data

Earlier in this article, you were introduced to the concept of modules. As you can see, your index.js file is getting a little large with the hard-coded product data. Imagine if you had customer, employee, and other data that you wished to return from different API calls. You can imagine that your index.js file would get unmanageable. Let's add a module to hold the product data and functionality. Add a new folder to your project and set the name to repositories. In the new repositories folder, add a new file named product-file.js and put the code shown in Listing 3 into this new file.

Listing 3: Create a module just to hold product data.

// Product repository object
let repo = {};

// Retrieve an array of product objects
repo.get = function () {
  return [
    {
      "productID": 879,
      "name": "All-Purpose Bike Stand"
    },
    {
      "productID": 712,
      "name": "AWC Logo Cap"
    },
    {
      "productID": 877,
      "name": "Bike Wash - Dissolver"
    },
    {
      "productID": 843,
      "name": "Cable Lock"
    },
    {
      "productID": 952,
      "name": "Chain"
    }
  ];
}

module.exports = repo;

Now that you have the product module created, open the index.js file and just before the app.get() call, load the repository, and make a call to the get() function created on that repository object, as shown in Listing 4.

Listing 4: Get the data from your new product repository object.

// Load product repository module
const repo = require('./repositories/product-file');

// GET Route
app.get('/', (req, res, next) => {
  // Get products from repository object
  let products = repo.get();
  
  res.send({
    "status": 200,
    "statusText": "OK",
    "message": "All products retrieved.",
    "data": products
  });
});

Try It Out

Save the changes to all the files in your project. Resubmit the same request using your browser or Postman to ensure that you're still getting the same array of product objects.

Read Product Data from a File

Instead of hard-coding the product array in JavaScript, create an array of product objects within a JSON file. You can then read that file and return that JSON data to the consumer of your API call. Add a new folder to your project named db. Add a file in this new db folder named product.json. Place the code shown in Listing 5 into this new file.

Listing 5: Create a file full of product objects.

[
{
  "productID": 706,
  "name": "HL Road Frame - Red, 58",
  "productNumber": "FR-R92R-58",
  "color": "Red",
  "standardCost": 1059.3100,
  "listPrice": 1500.0000
},
{
  "productID": 707,
  "name": "Sport-100 Helmet, Red",
  "productNumber": "HL-U509-R",
  "color": "Red",
  "standardCost": 13.0800,
  "listPrice": 34.9900
},
{
  "productID": 708,
  "name": "Sport-100 Helmet (Wireless), Black",
  "productNumber": "HL-U509",
  "color": "Black",
  "standardCost": 13.0863,
  "listPrice": 79.9900
},
{
  "productID": 709,
  "name": "Mountain Bike Socks, M",
  "productNumber": "SO-B909-M",
  "color": "White",
  "standardCost": 3.3963,
  "listPrice": 9.5000
},
{
  "productID": 710,
  "name": "Mountain Bike Socks, L",
  "productNumber": "SO-B909-L",
  "color": "White",
  "standardCost": 3.3963,
  "listPrice": 9.5000
},
{
  "productID": 711,
  "name": "Sport-100 Helmet, Blue",
  "productNumber": "HL-U509-B",
  "color": "Blue",
  "standardCost": 13.0863,
  "listPrice": 34.9900
},
{
  "productID": 712,
  "name": "AWC Logo Cap",
  "productNumber": "CA-1098",
  "color": "Multi",
  "standardCost": 6.9200,
  "listPrice": 8.9900
},
{
  "productID": 713,
  "name": "Long-Sleeve Logo Jersey, S",
  "productNumber": "LJ-0192-S",
  "color": "Multi",
  "standardCost": 38.4923,
  "listPrice": 49.9900
},
{
  "productID": 714,
  "name": "Long-Sleeve Logo Jersey, M",
  "productNumber": "LJ-0192-M",
  "color": "Multi",
  "standardCost": 38.4923,
  "listPrice": 49.9900
},
{
  "productID": 715,
  "name": "Long-Sleeve Logo Jersey, L",
  "productNumber": "LJ-0192-L",
  "color": "Multi",
  "standardCost": 38.4923,
  "listPrice": 49.9900
}
]

Open the respositories\product-file.js file and replace the entire contents of the file with the code shown in Listing 6. In this code, you first load the Node.js file system module. Create a constant named DATA_FILE to hold the name of the file where the array of product objects is located. Within the get() function, call the readFile() function on the file system object. The readFile() function is an asynchronous call to read a file. Once the file reading is done, the resolve() callback is invoked and the data read from the file is passed to resolve()``. If an error occurs while reading the file, the reject()callback is invoked and an error object is passed to thereject()` function.

Listing 6: Modify your product repository to read product data from a file.

// Load the node file system module
const fs = require('fs');

// Path/file name to the product data
const DATA_FILE = './db/product.json';

// Product repository object
let repo = exports = module.exports = {};

// Retrieve an array of product objects
repo.get = function (resolve, reject) {
  // Read from the file
  fs.readFile(DATA_FILE, function (err, data) {
    if (err) {
      // ERROR: invoke reject() callback
      reject(err);
    }
    else {
      // SUCCESS: Convert data to JSON
      let products = JSON.parse(data);
      // Invoke resolve() callback
      resolve(products);
    }
  });
}

You need to change the call to the get() function from within the app.get() function to ensure that it runs asynchronously. When calling the get() function, you need to supply two callback functions that correspond to the reject and resolve parameters accepted by the get() function. The basic syntax looks like the following code snippet:

repo.get(
  function(data) { 
    // Code to execute when call is successful
  },
  function (err) { 
    // Code to execute when an error occurs
  }
);

Open the index.js file and replace the app.get() call with the code shown in Listing 7. In this code, you still make a call to the repo.get() function, but you're passing two arguments as functions to it. The first argument passed to the get() function is the callback function that receives the product data if the call is successful. A JSON object is created with the normal properties and the data and then is sent to the caller of the API. The second argument passed to the get() function is the callback function that receives the error object. This error object is passed along to the next middleware code within the Express middleware chain using the next() function call. You'll learn more about Express exception handling in Part 2 of this article.

Listing 7: Pass in two callback functions to receive good data or an error object.

// GET Route
app.get('/', (req, res, next) => {
  repo.get(function (data) {
    // SUCCESS: Data received
    res.send({
      "status": 200,
      "statusText": "OK",
      "message": "All products retrieved.",
      "data": data
    });
  }, function (err) {
    // ERROR: pass error along to 
    // the 'next' middleware
    next(err);
  });
});

Try It Out

Save the changes to all the files in your project. Resubmit the request to the API to ensure that you're now receiving the data from the product.json file.

Get a Single Product of Data

Instead of always returning the complete list of product data, you may just want to allow a consumer of your API to pass in a product ID to return a single product object. You're dealing with a small amount of JSON objects in the product.json file, so let's just read the complete file each time and then locate the specific product object requested by the user. If you were using a MongoDB, MySQL, or a SQL Server database, you'd submit a query to retrieve a single piece of data instead of reading in the complete file.

Open the repositories\product-file.js file and add a new function to the repo object named getById(). Place this code just before the module.exports = repo line at the end of the file. To this function, pass the product ID along with your two callback functions for success or failure, as shown in Listing 8. After the file is read successfully, parse the JSON read into an array of product objects. Apply the find() method to the array to locate the product where the productID property matches the id parameter passed in. If the product is found, the product object is passed to the resolve callback function; otherwise the value undefined is passed.

Listing 8: Add a function to retrieve a single product object.

// Retrieve a single product object
repo.getById = function (id, resolve, reject) {
  fs.readFile(DATA_FILE, function (err, data) {
    if (err) {
      // ERROR: invoke reject() callback
      reject(err);
    }
    else {
      // SUCCESS: Convert data to JSON
      let products = JSON.parse(data);
      // Find the row by productID
      let product = products.find(
        row => row.productID == id);
      // Invoke resolve() callback
      // product is 'undefined' if not found
      resolve(product);
    }
  });
}

Open the index.js file and below the call to the app.get("/", ...) route you already have there, add the new route shown in Listing 9. To call this new route, make a request to the web server followed by the product ID, such as http://localhost:3000/711. The 711 at the end of the URL is the product ID you wish to locate within the product array read from the product.json file. You tell the route you want a parameter passed in by adding the colon (:) and the variable name, id, in the first parameter to the app.get() call.

Listing 9: Add a route to retrieve a single product object.

// GET /id Route
app.get('/:id', (req, res, next) => {
  repo.getById(req.params.id, function (data) {
    // SUCCESS: Data received
    if (data) {
      // Send product to caller
      res.send({
        "status": 200,
        "statusText": "OK",
        "message": "Single product retrieved.",
        "data": data
      });
    }
    else {
      // Product not found
      let msg = `The product '${req.params.id}' could not be found.`;
      res.status(404).send({
        "status": 404,
        "statusText": "Not Found",
        "message": msg,
        "error": {
          "code": "NOT_FOUND",
          "message": msg
        }
      });
    }
  }, function(err) {
    // ERROR: pass error along to 
    // the 'next' middleware
    next(err);
  });
});

Retrieve the product ID using the req.params.id property. The id at the end of the req.params property is the same name as the parameter you created in the app.get("/:id"). That product ID is passed to the repo.getById() function. If no errors occur in the repo.getById() call, the first callback function is invoked and a check is made to see if the data parameter contains a JSON object. If there is data, the standard JSON object is built with this product object and sent to the caller. If the data parameter is undefined, the product ID wasn't found in the array and a 404 response object is sent to the caller.

Try It Out

Save the changes made to all the files in your project and submit a request via your browser or Postman to the route http://localhost:3000/711. If you did everything correctly, you should get a valid product object returned that looks like the following:

{
  "status": 200,
  "statusText": "OK",
  "message": "Single product retrieved.",
  "data": {
    "productID": 711,
    "name": "Sport-100 Helmet, Blue",
    "productNumber": "HL-U509-B",
    "color": "Blue",
    "standardCost": 13.0863,
    "listPrice": 34.99
  }
}

Now try entering an invalid product ID by submitting the request http://localhost:3000/1. The number one is an invalid product ID, so you should get a 404 response object, as shown in the following code snippet.

{
  "status": 404
  "statusText": "Not Found",
  "message": "The product '1' could not be found.",
  "error": {
    "code": "NOT_FOUND"
    "message": "The product '1' could not be found."
  }
}

Searching for Product Data

Instead of searching for a product only on the primary key (product ID) field, you might want to search on one or more of the other properties on the product object. For example, you might want to see if the product name contains a certain letter or letters. Or you might want to check whether the list price is greater than a specific value passed in. To accomplish this, submit a request to a search route passing in some URL arguments like any of the following requests shown below.

/search?name=Sport

/search?listPrice=100
/search?name=Sport&listPrice=100

You can then grab these URL parameters from the request object and create your own JSON object to submit to a search() function you're going to create in your product repository object.

let search = {
  "name": req.query.name,
  "listPrice": req.query.listPrice
};

Create a Search Function

Open the repositories\product-file.js file and create a search() function (Listing 10) below the getById() function you added earlier. The search() function accepts the search object shown above and uses the properties to filter the records from the array of products and only return those products that match the criteria specified in the search object.

Listing 10: The search function can return one or more rows.

// Search for one or many products
repo.search = function (search, resolve, reject) {
  if (search) {
    fs.readFile(DATA_FILE,function (err, data) {
      if (err) {
        // ERROR: invoke reject() callback
        reject(err);
      }
      else {
        // SUCCESS: Convert data to JSON
        let products = JSON.parse(data);
        // Perform the search
        products = products.filter(
          row => (search.name ? row.name.toLowerCase().indexOf(
              search.name.toLowerCase())
                >= 0 : true) &&
            (search.listPrice ? parseFloat(row.listPrice) > 
                parseFloat(search.listPrice) : true));
        // Invoke resolve() callback
        // Empty array if no records match
        resolve(products);
      }
    });
  }
}

For the purposes of this article, I'm just using the JavaScript filter() function to perform the searching through the product array. If you were using a SQL or a NoSQL database, you'd use their query functionality. The filter() method is applied to the array and passed a lambda expression to filter the rows. In the expression shown above, the name property in the product object and the name object in the search object are converted to lower case letters before seeing if one is contained in the other. Convert the listPrice properties in both the search object and product object to floating point types before performing a greater than comparison. If both these conditions evaluate to true, that row is returned into the final resulting products array.

Now that you have the search() function created in the product repository, it's time to add a route on your web server to call that function. Open the index.js file and add the code shown in Listing 11 before the call to the app.get('/:id',...). You need to put this search route prior to the route that retrieves a single product, or it may try to map either the name or list price to the id parameter in that route.

Listing 11: The search route builds a search object to locate rows of product data.

// GET /search Route
app.get('/search', (req, res, next) => {
  // Create search object with 
  // parameters from query line
  let search = {
    "name": req.query.name,
    "listPrice": req.query.listPrice
  };
  if (search.name || search.listPrice) {
    repo.search(search, function (data) {
      // SUCCESS: Data received
      if (data && data.length > 0) {
        // Send array of products to caller
        res.send({
          "status": 200,
          "statusText": "OK",
          "message": "Search was successful.",
          "data": data
        });
      }
      else {
        // No products matched search
        let msg = `The search for '${JSON.stringify(search)}' was not successful.`;
        res.status(404).send({
          "status": 404,
          "statusText": "Not Found",
          "message": msg,
          "error": {
            "code": "NOT_FOUND",
            "message": msg
          }
        });
      }
    }, function (err) {
      // ERROR: pass error along to the 'next' middleware
      next(err);
    });
  }
  else {
    // No search parameters passed
    let msg = `No search parameters passed in.`;
    res.status(400).send({
      "status": 400,
      "statusText": "Bad Request",
      "message": msg,
      "error": {
        "code": "BAD_REQUEST",
        "message": msg
      }
    });
  }
});

Try It Out

Save all the changes made to the files in your project and submit the request http://localhost:3000/search?name=Sport via your browser or Postman. This request should return a few records where the product name contains the word “Sport”. Submit the request http://localhost:3000/search?name=Sport&listPrice=50 to look for both a name that contains the value “Sport” and the listPrice is greater than or equal to 50. From this request, you should have a single product object returned. Finally, submit the request http://localhost:3000/search?name=aaa to check your error handling. From this request, you should get no records and thus a 404 status code and object is returned from the query.

Use Router Object and Add API Prefix

If you've created Web API calls before, you know that most developers like to put the prefix api in front of every route. This helps you distinguish between a route that returns some HTML versus a route that just returns data. To accomplish this with Express, you need to use the router object instead of the app object. Open the index.js file and add the code shown in the snippet below just before the declaration of the const post = 3000.

// Create an instance of a Router
const router = express.Router();

Next, modify all of the route calls you created that start with app.get() to router.get() as shown in the snippet below:

router.get('/', (req, res, next) => {
  // REST OF THE CODE HERE
}
router.get('/search', (req, res, next) => {
  // REST OF THE CODE HERE
}
router.get('/:id', (req, res, next) => {
  // REST OF THE CODE HERE
}

Scroll down toward the end of the index.js file and immediately before the call to the app.listen() function, add the following code to setup the prefix for all routes included within the router object.

// Configure router so all routes are prefixed with /api
app.use('/api', router);

Try It Out

Save all the changes made to the files in your project and submit the request http://localhost:3000 via your browser or Postman. After submitting this request, you should get a 404 status code and some HTML that says "Cannot GET /". This means that there's no route defined on the root of your web application. Change the request to include the /api suffix to the web application root: http://localhost:3000/api. From this request, you should once again retrieve the array of product objects.

Modularize Your API Calls

As you can see, the index.js file is getting quite large. You can imagine that if you add more routes to handle inserting, update, and deleting products, this file is going to get huge. Think about adding on more routes for customers, employees, and other data in your web application. The result would be a maintenance nightmare. Let's move all product routes into a module that can then be loaded into the index.js file using the require() function. Add a new folder to your project named routes. Within the routes folder, add a new file named product.js. Add the code shown in the code snippet below into this new file.

// Create an instance of a Router
const router = require('express').Router();

// Load product repository module
const repo = require('../repositories/product-file');

// MOVE YOUR ROUTES HERE FROM INDEX.JS

module.exports = router;

Open the index.js file and cut out all the router.get() routes you created and paste them into the routes\product.js file at the location where the comment // MOVE YOUR ROUTES HERE FROM INDEX.JS is located. You can remove this comment after you've pasted in all the routes. You can also remove the following code from the index.js file:

// Load product repository module
const repo = require('./repositories/product-file');

Open your index.js file and, from where you cut out all the routes, add the following code to register the routes that are now created within the routes\product.js file.

// Mount routes from modules
router.use('/product', require('./routes/product'));

This line of code adds another prefix, /product, to all the routes contained within the product module. You want to add this additional prefix because if you add customer routes, you'll want to use /api/customer for all those routes. And, if you add employee routes, you'll want to use /api/employee, and so on. The index.js file should now look exactly like what you see in Listing 12. As you can see from the code in Listing 12, the index.js file is now significantly smaller. If you add more routes, say for customers or employees, you only add one additional line in the index.js file for each route you add. All of the route definitions are placed into the corresponding files within the routes folder.

Listing 12: Your index.js file is now significantly smaller and easier to maintain.

// Load the express module
const express = require('express');
// Create an instance of express
const app = express();
// Create an instance of a Router
const router = express.Router();
// Specify the port to use for this server
const port = 3000;

// Mount routes from modules
router.use('/product', require('./routes/product'));

// Configure router so all routes are prefixed with /api
app.use('/api', router);

// Create web server to listen on the specified port
let server = app.listen(port, function () {
  console.log(`AdvWorksAPI server is running on http://localhost:${port}.`);
});

Try It Out

Save all the changes made to the files in your project and submit the request http://localhost:3000/api/product via your browser or Postman. Check out a couple of the other routes by submitting the request http://localhost:3000/api/product/711 to retrieve a single product object. Now try the search functionality by submitting the request http://localhost:3000/api/product/search?name=Sport. All three of the above requests should return the same data as they did when they were defined within the index.js. The only difference is the prefix /api/product and the fact that all the routes are declared within a separate file in your web application.

Inserting Data (POST)

Chances are that you're not going to write an API that simply returns data. You most likely will need to modify data as well. To insert data, you use the POST verb and create a route that maps to that request. Open the repositories\product-file.js` file and add a new function named insert(), as shown in Listing 13.

Listing 13: Create an insert method to map to the POST route.

// Insert a new product object
repo.insert = function (newData,
  resolve, reject        ) {
  fs.readFile(DATA_FILE, function (err, data) {
    if (err) {
      // ERROR: Invoke reject() callback
      reject(err);
    }
    else {
      // SUCCESS: convert data to JSON
      let products = JSON.parse(data);
      // Add new product to array
      products.push(newData);
      // Stringify the product array
      // Save array to the file
      fs.writeFile(DATA_FILE, 
        JSON.stringify(products), 
        function (err) {
        if (err) {
          // ERROR: Invoke reject() callback
          reject(err);
        }
        else {
          // SUCCESS: Invoke resolve() callback
          resolve(newData);
        }
      });
    }
  });
}

In this function, you pass in a new product object to the parameter newData. The complete product.json file is read in and converted to a JSON array. Use the push() method to add the new product object to the end of the array. Stringify the JSON array and write the new array to the product.json file. Please remember that this code is for learning purposes only. You wouldn't want to use this rudimentary file I/O in a production application. Instead, you'd use a real database that can handle multiple requests at the same time.

Add JSON Middleware

When posting data to a route hosted by Express, some middleware needs to be added to allow Express to take the string version of the JSON data and convert it to a real JSON object. Open the index.js file and add the following code after the const router = express.Router() call to add this middleware:

// Configure JSON parsing in body of request object
app.use(express.json());

Open the routes\product.js file and scroll down towards the bottom of the file. Just before the call to module.exports = router, add a new router.post() method, as shown in Listing 14. This code extracts the JSON object from the req.body property and passes it to the insert() function you just created.

Listing 14: Add a router.post to map the insert() function to this route.

// POST Route
router.post('/', function (req, res, next) {
  // Pass in the Body from request
  repo.insert(req.body, function(data) {
    // SUCCESS: Return status of 201 Created
    res.status(201).send({
      "status": 201,
      "statusText": "Created",
      "message": "New Product Added.",
      "data": data
    });
  }, function(err) {
    // ERROR: pass error along to 
    // the 'next' middleware
    next(err);
  });
});

Try It Out

Save the changes made to all the files in your project. Open Postman or another API test tool, and submit a POST with the JSON object contained in the body of the post back. Referring to Figure 6, follow the steps below to submit a new product object to be inserted into the product.json file.

  • Select POST from the drop-down and fill in the URL to call your Web API.
  • Click on the Body tab.
  • Change the first drop-down to Raw.
  • Change the second drop-down to JSON.
  • In the Body text box add the following JSON object.
{
  "productID": 986,
  "name": "Mountain-500 Silver, 44",
  "productNumber": "BK-M18S-44",
  "color": "Silver",
  "standardCost": 308.2179,
  "listPrice": 564.9900
}

Click the Send button to call the POST route you created. When the response returns, you should see the data in the lower window of Postman (#6). Go back to VS Code and open the db\product.json file. At the end of the file, you should see the product you submitted from Postman.

Figure 6: Use Postman to submit a POST to the Web API.
Figure 6: Use Postman to submit a POST to the Web API.

Updating Data (PUT method)

Let's now add the functionality to update a product in your product.json file. Open the repositories\product-file.js file and add a new function named update(), as shown in Listing 15. This function reads all the text from the product.json file and converts the data to a JSON array. Next, locate the row in the array to update using the find() method and search for the product ID passed into this function. If the product is found, use the Object.assign() method to merge the new data with the data in the existing product object. Stringify the product array and write all the data back to the product.json file.

Listing 15: Create an update method to map to the PUT route.

// Update an existing product object
repo.update = function (changedData, id, resolve, reject) {
  fs.readFile(DATA_FILE, function (err, data) {
    if (err) {
      // ERROR: Invoke reject() callback
      reject(err);
    }
    else {
      // SUCCESS: Convert to JSON
      let products = JSON.parse(data);
      // Find the product to update
      let product = products.find(row => row.productID == id);
      if (product) {
        // Move changed data into corresponding properties of the existing object
        Object.assign(product, changedData);
        // Stringify the product array
        // Save array to the file
        fs.writeFile(DATA_FILE, JSON.stringify(products), function (err) {
          if (err) {
            // ERROR: Invoke reject() callback
            reject(err);
          }
          else {
            // SUCCESS: Invoke resolve() callback
            resolve(product);
          }
        });
      }
    }
  });
}

Open the routes\product.js file and scroll down toward the bottom of the file. Just before the call to module.exports = router, add a new router.put() function, as shown in Listing 16.

Listing 16: Add a router.put() route to map to the update() function.

// PUT Route
router.put('/:id', function (req, res, next) {
  // Does product to update exist?
  repo.getById(req.params.id, function (data) {
    // SUCCESS: Product is found
    if (data) {
      // Pass in Body from request
      repo.update(req.body, req.params.id, function (data) {
        // SUCCESS: Return 200 OK
        res.send({
          "status": 200,
          "statusText": "OK",
          "message": `Product '${req.params.id}' updated.`,
          "data": data
        });
      });
    }
    else {
      // Product not found
      let msg = `The product '${req.params.id}' could not be found.`;
      res.status(404).send({
        "status": 404,
        "statusText": "Not Found",
        "message": msg,
        "error": {
          "code": "NOT_FOUND",
          "message": msg
        }
      });
    }
  }, function(err) {
    // ERROR: pass error along to the 'next' middleware
    next(err);
  });
});

Try It Out

Save the changes made to all the files in your project. Open Postman or another API test tool and submit a PUT with the JSON object contained in the body of the post back. Just like you did when inserting, select PUT from the drop-down. Within the Body tab, add the following JSON object. Because you're only making changes to a couple of the properties of the product object, Object.assign() only updates those properties it finds in this JSON object and applies them to the product object read from the file.

{
  "productID": 986,
  "name": "Mountain-500 Gray, 44",
  "listPrice": 699.00
}

Modify the URL in Postman to add the product ID you wish to update at the end of the request http://localhost:3000/api/product/986. Click the Send button to submit the PUT request. Go back to VS Code, open the db\product.json file, and format the file. Scroll down to the bottom and view the changes made to product 986.

Return a 404

If you want, try out the 404 Not Found error by appending an invalid product ID to the URL and submit the request http://localhost:3000/api/1.

Delete Data

Besides POST and PUT routes, add the ability to delete a product by adding a DELETE route as well. Open the repositories\product-file.js file and add a new function named delete(), as shown in Listing 17. The delete() function reads in the complete product file and converts it to a JSON object array. Use the findIndex() method to locate the product ID that matches the ID passed into the delete() function. If found, a number greater than minus one (-1) is returned. This is the index number in the array where the product is located. Use the splice() method on the array to remove that row from the array. Stringify the array and write the new array of products back to the product.json file.

Listing 17: Create a delete function to map to the DELETE route.

// Delete an existing product object
repo.delete = function (id, resolve, reject) {
  fs.readFile(DATA_FILE, function (err, data) {
    if (err) {
      // ERROR: Invoke reject() callback
      reject(err);
    }
    else {
      // SUCCESS: Convert data to JSON
      let products = JSON.parse(data);
      // Find product to delete
      let index = products.findIndex(
        row => row.productID == id);
      if (index != -1) {
        // Remove row from array
        products.splice(index, 1);
        fs.writeFile(DATA_FILE,
          JSON.stringify(products),
          function (err) {
          if (err) {
            // ERROR: Invoke reject() callback
            reject(err);
          }
          else {
            // SUCCESS: Invoke resolve() callback
            resolve(index);
          }
        });
      }
    }
  });
}

Open the routes\product.js file and scroll down towards the bottom of the file. Just before the call to module.exports = router, add a new router.delete() function, as shown in Listing 18.

Listing 18: Add a router.delete() route to map to the delete() function.

// DELETE Route
router.delete('/:id', 
  function (req, res, next) {
  // Does product to delete exist?
  repo.getById(req.params.id, function (data) {
    // SUCCESS: Product is found
    if (data) {
      // Pass in 'id' from request
      repo.delete(req.params.id, function (data) {
        // SUCCESS: Return 204 No Content
        res.status(204).send();
      });
    }
    else {
      // Product not found
      let msg = `The product '${req.params.id}' could not be found.`
      res.status(404).send({
        "status": 404,
        "statusText": "Not Found",
        "message": msg,
        "error": {
          "code": "NOT_FOUND",
          "message": msg
        }
      });
    }
  }, function(err) {
    // ERROR: pass error along to 
    // the 'next' middleware
    next(err);
  });
});

Try It Out

Save the changes made to all the files in your project. Open Postman or another API test tool, and submit a DELETE with the product ID on the URL line http://localhost:3000/api/product/986. Click the Send button and you should receive a 204 No Content status code back from the request. There will be no content in the response body area; you'll simply see the 204 status code.

Summary

In this article, you learned to build a Node.js and Express project for hosting Web API calls. You created a set of routes to read, search, add, edit, and delete product data in a JSON file. A couple of reusable modules were built to help you compartmentalize your application and keep any one file from becoming too large. In the next article, you'll learn to read configuration settings from a JSON file, handle exceptions, and add the ability to interact with a SQL Server database.

Table 1: A list of tools commonly used to build Web APIs using Node.js
Tool Use
Node.js The web server environment upon which you build your web applications
npm The Node Package Manager used to publish, discover, and install Node frameworks such as Express
nodemon A small utility, installed via npm, to monitor your project directory and automatically restart your node application when changes occur in your files
Express A framework, installed via npm, to make it easy to build your Web API calls or a website
Postman A tool to help you test your Web API calls
Visual Studio Code An editor for typing in your JavaScript code and running your Web server