E-Commerce App with Node
I originally posted this tutorial on how to build an e-commerce backend with Node on my personal site but I wanted to repost it here for some exposure as my personal site doesn’t have as much traffic as here. So without further ado I present to you the e-commerce site.
I’m sure most people know what Node.js is these days. But for those of you that aren’t familiar or have heard the term and wanted to know a bit more about it. Node.js, or Node as it is more commonly called, is a non-blocking and asynchronous runtime for JavaScript. This means that frontend developers no longer have to learn a second language in order to create web servers and the backend logic behind our websites. No longer ill a frontend developer need to learn languages like Java, Python, or even Ruby and the Rails framework associated with it. Frontend developers can now use the skills they know for the client side with the server side.
Before we get into the nitty gritty and start writing code we will first need to install a few things. If this isn’t your first time with Node or JavaScript then please feel free to skip this part since you most likely already have Node installed. You will need to head on over to:
First things first, there are two different versions of Node that you can download. I recommend using the LTS version as it is the latest stable version of Node. Which means that it has less bugs and will introduce less bugs into your applications as majority of them have been fixed. However if you are the type of person that likes to try different things such as beta testing and you have the latest version of Chrome Canary installed so as to be the first to have new nightly features then I won’t stop you from installing the current release as that one will have the most up-to-date features. But be warned it could cause your apps to crash frequently and there might not be a fix for it just yet. Depending on your operating system you can follow these steps for Windows/macOS:
- Click the LTS button
- Run the installer
Open your Powershell or CMD prompt and type
node -v
- A message should pop up showing you the version that matches the installer you clicked on from the site.
If you are on Linux chances are you will be familiar with your package manager such as apt-get or, in my case, pacman as I am using a version of Arch Linux. The steps are as follows
- Open your terminal app
- type
sudo pacman -S nodejs npm
- Once it has finished type
node -v
2. If a message shows the version such as 16.3.0 then you are ready to go.
Now that all of the tools you will need are installed we can continue.
Let’s write a Simple web server so you can see the simplicity of Node in action. Open up your terminal and navigate to a directory that is easy to find. I typically use my Desktop but if you are on Windows and have Visual Studio installed it creates a source/repos directory for you that will allow you to store code projects. Follow the steps below
- Open the terminal and type:
cd Desktop
mkdir node-projects
cd node-projects
mkdir simple-server
cd simple-server
Open your newly created directory in your favorite editor mine is VS Code which I can open this directory by typing:
code .
The command above will open VS Code at my current working directory.
The code above will create a server and have it return a response to the browser that displays the message: hello world!
And that is all it takes to create a basic web server. Three lines of code, which can technically fit on one line but for readability purposes it’s a best practice to put them on a new line.
Let’s start to create our first API. From here on out we are going to be working on the e-commerce API. First and foremost we can close out of VS Code or whichever editor you are using. And inside your terminal we can go back to our node-projects directory by typing this command:
cd ..
The API we are building will focus on a production e-commerce app that can be modified to fit any starting place for just about any company that may need one. The app ill showcase PC parts for sale for those that want to build their own computers. We will be building this API to be used by mobile apps, CLIs and as a library. We are going to create a JSON file that will store the information of the products we are selling. Here’s what it will look like:
products.json
[ { "id": "jrP28HZBVI1", "description": "ASUS Motherboard", "imgThumb": "https://images.unsplash.com/photo-1522920192563-6df902920a8a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=60", “img”: “https://images.unsplash.com/photo-1522920192563-6df902920a8a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9& auto=format&fit=crop&w=400&q=60", “userId”: “Z1uhdL3Lmw”, “userName”: “Thomas Jensen”, “userLink”: “https://unsplash.com/@thomasjsn", “tags”: [ “motherboard”, “ASUS”, “computer”, “parts”, ] },]
Each product in the listing should have enough info for our client to use. The listing is already in a format that a client can readily use, so the API can start simple. We can make this API available at the endpoint http://localhost:5000/products. The JSON file above is meant to simulate a database. In an actual production application you wouldn’t store your data in a JSON file as that would be tedious and time consuming to keep track of. But for this demo application that is the route I chose so we could focus on Node concepts.
Now we can start fleshing this out. We are going to:
- Create an express server
- Listen for a GET request on our/products route
- Create a request handler to read from our products.json file and send the response
We will also set up some more advanced features:
- Create a client library
- Write a test verifying that it works as expected
- View our products via a separate web app
Express Server
We will start simple:
server.js
const fs = require('fs').promises;
const path = require('path');
const express = require('express');
const port = process.env.PORT || 5000;
const app = express();app.get('/products', listProducts);
app.listen(port, () => console.log(`Server listening on port ${port}`));async function listProducts(req, res) {
const productsFile = path.join(__dirname, '../products.json') try {
const datat = await fs.readFile(productsFile);
res.json(JSON.parse(data));
} catch (err) {
res.status(500).json({ error: err.messgae });
}
}
The above code sets up a very basic web server using Express. Express is a very popular library that allows us to create servers fairly easy and quickly. We will require the fs module which allows us to access the file system and we also use the path module to get the path to our products.json file so that we can use the data inside to display the data tot he website on the browser. If you are unfamiliar with the try/catch block the reason that we use this is so if there is an error we can catch it and produce a somewhat decent and understandable error message. The try block will, as the name says try to run the code within that portion of the block. If the code fails then the catch block will display the error message as to what failed so we can fix it.
Our API should now be up. The API will only return JSON and won’t care how the data is necessarily being used or rendered. In other words we have many ways that we can use it. The API can be hosted on a CDN, a CLI utility tool such as curl, a mobile app, or an NTML file. As long as the client used understands HTTP, everything should work fine.
We can run the API to see if it works by opening a terminal, if you are using VS Code you can go up to the to of the screen and select view then terminal and it will open a terminal right there within the editor already at the spot you will need. Then you will run:
node server.js
You should be able to go to a browser now and visit localhost:5000/products to see the data.
Basic Front End
We will start with a very simple HTML page that will fetch the API endpoint and use console.log() to display it.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Awesome Parts</title>
</head>
<body>
<script>
const url = "http://localhost:5000/products";
fetch(url).then(res => res.json()).then(products => console.log(products));
</script>
</body>
</html>
If you are using VS Code you can install a really handy extension called Live Server that will let you right click on the html file and select “Open with Live Server” it will automatically open a browser and launch your file.
You should now be able to press F12 or right click and select inspect if you are in Google Chrome, and then if you aren’t on it by default select the console tab. You should now see…a bunch of read text showing you some errors.
Since our HTML page is being served with a different origin than what our API is using, our browser is blocking the request. For security reasons, bowsers will not load data from other origins/domains unless the other server eturns the correct CORS(Cross-Origin Resource Sharing) headers. If the HTML file was served directly from our server we wouldn’t have this issue, but as we want to be able to have any web client access our API we need to add a single line to our route handler like so:
res.setHeader('Access-Control-Allow-Origin', '*');res.json(JSON.parse(data));
You should now be able to refresh the page, if it didn’t already refresh, and in the console tab that should still be open you will see the data being returned. If you find yourself getting an error when trying to get your data from an API in the future make sure that you have that one line of code and if not add it and try again. Nine times out of ten that one line will fix the error.
Since this post is about Node and setting up APIs not directly dealing with the front-end we are going to move on but if you are familiar with writing front-end code and want to add a better looking front-end to consume this API by all means feel free. I might write a tutorial later using React to consume the API. We will see.
Creating A Library
We now have a basic API setup and running. We are now ready to modularize our app so that we can keep things neat and clean for when we start adding more functionality. A single-file production API would be extremely difficult to maintain.
The module system that Node uses is one of the best things about using Node. We are able to leverage this to make sure that our app’s functionality is divided into individual modules with clear boundaries.
So far we have been using the require() method to pull in core modules that are built directly in to Node such as the http and third party modules like express. We are going to see how we can use the require() method to import local files. We will be pulling our route handlers out into their own file. We are going to name the file app.js and we will call the file inside of our server.js file like so:
server.js
const express = require('express');
const api = require('./app');
const port = process.env.PORT || 5000;
const app = express();app.get('/products', api.listProducts);
app.listen(port () => console.log(`Server listening on port ${port}`));
Here is app.js
app.js
const Products = requir('./products') module.exports = { listProducts }async function listProducts(req, res) { res.setHeader('Access-Control-Allow-Origin', '*') try { res.json(await Products.list()) } catch (err) { res.status(500).json({ error: err.message }) } }
We use require() to pull in another module, ./products. This will be our data model for our products. We will keep the data model separate from the API module because the data model doesn’t need to know about HTTP request and response objects. The API module is responsible for converting HTTP request into HTTP responses. This is achieved by the module leveraging other modules. Another way of thinking about this is that this module is the connection point to the outside world and our internal methods. If you come from a MVC background such as Django or Ruby on Rails this would be like the Conroller.
Let’s build the products module
products.js
const fs = require('fs').promises;
const path = require('path');
const productsFile = path.join(__dirname, '../products.json');module.exports = {
list
}async function list() {
const data = await fs.readFile(productsFile);
return JSON.parse(data);
}
As of right now, our products module only has a single method, list(). Also take a note that this module is general-purpose, we want to keep it this way so that it can be used by our API, a CLI tool or even tests. A similar thought process is to think about the server module being on the outside. This module is responsible for creating a web server object, setting up middleware (we will touch on this shortly), and connecting routes to route handler functions. Basically our sever module connects external URL endpoints to internal route handler functions.
Our API module is a collection of route handlers. Each route handler is responsible for accepting a request object and returning a response. The route handler does this primarily by using model methods. Our goal is to keep these route handler functions high-level and readable.
Last but certainly not least we have our model module. Our model is on the inside, and should be agnostic about how it’s being used. The route handler functions can only be used in a web server because they expect response objects and are aware of things like content-type headers. In other words the model methods are only concerned with getting and storing data.
Query Parameters
Our API returns the same data each time. This means that ther’s no reason for our API to exist. The products.json file, which is a static file that could be served from a server or CDN, could technically achieve what our API does. We need to have our API respond with different data depending on the client’s needs. The first thing a client will need is a way to request only the data that it wants.
The goal is to accept two query parameters that are only necessary if the client wants to fetch only a specific amount of data and not all products at once. These parameters are going to be limit and query. By allowing the client the ability to specify how many results they receive at a time and how many results to skip, we can allow the client to scan the catalog at its own pace.
An example could be if the client wants the first 50 items, it can make a request to /products?limit=50, and if the client wants the next 50 items, it can make a request to /products?limit=50&offset=50.
Express can parse query parameters for us. This means that when a client hits a route like /products?limit=50&offset=50 we should be able to access an object like:
{
limit: 50,
offset: 50
}
Let’s update our request handler to use the built in feature of express.
app.js
async function listProducts(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
const { offset = 0, limit = 50 } req.query;
try {
res.json(await Products.list{
offset: number(offset),
limit: Number(limit)
}))
} catch(err) {
res.status(500).json({ error: err.message });
}
}
The above changes show us creating two variables offset and limit from the req.query object that express provides. We also make sure that they have default values if one or both are not set. We also coerce the limit and offset variables into numbers as by default query parameter values are strings.
Let’s make a minor change to our file:
products.js
async function list (opst = {}) {
const { offset = 0, limit = 25} = opts;
const data = await fs.readFile(productsFile);
return JSON.parse(data).slice(offset, offset + limit);
}
We should now be in a decent spot to see if our changes are working. Go ahead and run your server and navigate to http://localhost:5000/products?limit=1&offset=1
In another post I will show you how we can view our APIs with a really awesome tool called Postman. For now using the browser will work. But keep in mind with most API development, the tool of choice is to use Postman or something similar like Insomnia.
Filtering Our Products
At this point we should now have a good understanding of how to add functionality to our app. We are going to use the same approach to add a filter by tag. We can accept a specified tag filter via a query parameter
app.js
async function listProducts(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
const { offset = 0, limit = 50, tag } = req.query;
try {
res.json(await Products.list({
offset: Number(offset),
limit: Number(limit),
tag
}))
} catch (err) {
res.status(500).json({ error: err.message });
}
}
We are making sure to pull the tag property from the req.query object and pass it as an option to Products.list(). Because all query parameters come in aas string, and we expect tag to be a string, we don’t need to coerce it to a type as the default type is string.
Our handler is updated but right now, our Products.list() method has not been changed to accept the tag option. If we were to test this endpoint, filtering wouldn’t work yet. Let’s update our products.js file
products.js
async function list (opts = {}) {
const { offset = 0, limit = 50, tag } = opts;
const data = await fs.readFile(productsFile);
return JSON.parse(data)
.filter((p, i) => !tag || p.tags.indexOf(tag) >= 0)
.slice(offset, offset + limit);
}
We have restricted our response to a single tag by adding in the filter() method.
Fetching A Single Item
The app is coming along pretty well. We are able to page through all of our products and to filter results by tag. The next thing we need is the ability to get a single product.
We’re going to crate a new route, handler and model to add this feature. Let’s add the new route to our server.js file.
server.js
const express = require('express');
const api = require('./app');
const port = process.env.PORT || 5000;
const app = express();app.get('./products', api.listProducts);
app.get('./products/:id', api.getProduct);const server = app.listen(port, () => console.log(`Server listening on port ${port}`));
The route we are using is a pretty standard route. We want express to listen to any route that starts with /products/ and is two levels deep. This route will match the folowing urls, /products/1, /products/xyz, or /products/some-super-long-string. Express will make anything in the place of :id available to our route handler on the req.params.id property.
Let’s now add the getProduct() method.
app.js
module.exports = {
getProduct,
listProducts
}async function getProducts(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
const { id } = req.params;
try {
const product = await Products.get(id);
if (!product) return next();
res.json(product)
} catch (err) {
res.status(500).json({ error: err.message });
}
}
Here we are seeing something new. The next() method in our handler function is used in the case that our item isn’t found. The other new thing is the req.params to get the desired product ID from the request. I will touch more on next() later.
Let’s update Products.js
products.js
async function get(id) {
const products = JSON.parse(await fs.readFile(productsFile));
for (let i = 0; i < products.length; i++) {
if (products[i].__id === id) return products[i]
}
return null;
}
We are just iterating through the items until we find the one with the matching ID. If we don’t find one we return null. The next section will go into more detail about middleware.
Next() Up Middleware
First we have some refactoring we need to do. If you open the app.js file you will see there is some repetitiveness that can be taken care of. Both methods set the Access-Control-Allow-Origin header for CORS as well as use a try/catch to send JSON. We can use some middleware to put this logic in a single place.
We have used the request handlers that immediately correspond to HTTP methods such as GET. There are others that we will touch on later such as PUT, POST and DELETE. I want to talk about middleware here though. We can setup a call that, regardless of what the HTTP method or URL expected, will still function. Let’s update our server.js file.
server.js
app.use(middleware.cors);
app.get('./products, api.listProducts);
app.get('./products/:id', api.getProducts);
We will create a new module middlware.js to store our functions.
middleware.js
const cors (req, res, next) => {
const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin || '*');
res.setHeader(
'Access-Control-Allow-Methods',
'POST, GET, PUT, DELETE, OPTIONS, XMODIFY'
); res.setHeader('Access-Control-Allow-Credentials', true);
res.setHeader('Access-Control-Max-Age', '86400');
res.setHeader(
'Access-Control-Allow-Headers',
'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'
);
next();
}
This file is storing a bunch of CORS headers. By default browsers are very picky about what request types are allowed. We can set these headers to tell the browser to be more permissive when request are made from pages hosted on other domains/origins. As you may have noticed we are also calling next().
Next() is a function that is provided to all request handlers; this includes both route handlers and middleware functions. Next() allows our functions to pass control of the request and response objects to other handlers in a series.
The order in which these handlers is defined is by the order in which they are called. If you look back at your file you will see app.use(middleware.cors) before we call app.get() for each of our route handlers. This means that our middleware function will run before either of those route handlers have a chance to. And if we didn’t use next() the route handlers wouldn’t be called.
Let’s update app.js
app.js
const Products = require('./products');
module.exports = {
getProduct,
listProducts
}async function getProduct(req, res, next) {
const { id } = req.params;
try {
const product = await Products.get(id);
if (!product) return next();
res.json(product)
} catch (err) {
res.status(500).json({ error: err.message });
}
}async function listProducts(req, res) {
const { offset = 0, limit = 50, tag } = req.query;
try {
res.json(await Products.list({
offset: Number(offset),
limit: Number(limit),
tag
})))
} catch (err) {
res.status(500).json({ error: err.message });
}
}
CORS still works? Crazy right? Our middleware.sj module now handles that for us instead of the app.js file.
Error Handling
We have officially centralized our CORS header. We can now do something similar with our error handling. We are using the try/catch block in both of our route handler functions. Let’s move that out into a function in our middleware.
middleware.js
function handleError(err, req, res, next) {
console.error(err);
if (res.headersSent) return next(err);
res.status(500).json({ error: 'Internal Error' });
}
The first argument we pass to this new function is err. This function will receive the error object that is passed to next() along with the other standard arguments, req and res. Console.error() will log the errors for us and not in the response for the client to see. Error logs often contain sensitive information, and it can be a security risk to expose them to API clients.
Express comes with it’s own 404 Not Found error handling but we want our eror to be handled with JSON. Let’s go ahead and build that function. I will use ES6 functions just to show you the difference. This syntax is the newer way to create functions but you can use both.
middleware.js
const notFound = (req, res) => {
res.status(404).json({ error: 'Not Found' });
}
The function above is a normal middleware function so we are not looking for an error object. The trick is that we only want this to run after all routes have been checked. If no route handlers match the request’s URL, this function should run. This function will also run if any route handler matches, but calls next(), with no arguments.
Let’s add this functionality to our server.js.
server.js
app.use(middleware.cors);
app.get('/products', api.listProducts);
app.get('/products/:id', api.getProduct);
app.use(middleware.handleError);
app.use(middleware.notFound);
The lgic for how express will handle the requests is pretty straight forward. It will basically go from top to bottom. First, all requests will run through middleware.cors(). The request URL will then be matched against our two route handlers. If a route handler does not match the request URL, middleware.notFound() is ran, if there is the corresponding route handler will run. While that route handler is running, if there’s an error and next(err) is called, middleware.handleError() will run.
HTTP POST, PUT, and DELETE
I said that we would be implementing these other HTTP methods soon. As promised let’s go ahead and implement them. GET requests are one of the most simplest and common request, but they limit the info that a client can send to an API. We aren’t going to go into too much detail in this post. So for now we are only going to concentrate on receiving and parsing data from the client.
When a GET request is sent the only information transferred is the URL path, host and request headers. GET requests are not supposed to have any permanent effect on the server. To provide richer funtionality to the client we need to support other methods like POST.
A POST can contain a request body. the request body can be any type of data such as a JSON document, form field, or a movie file. An HTTP POST is the primary way for a client to create documents on a server.
Our admin users will be able to use the client to create new products. For now, all users will have this power, as I won’t be covering authentication and authorization for admin users in this post.
Let’s update our server.js file.
server.js
async function createProduct(req, res, next) {
console.log('request body:', req.body);
res.json(req.body);
}
We are interested in storing a new product for use later. Before we can store a product, we should be able to access it. Since we’re expecting the client to send the product as a JSON document for the request body, we should be able to see it via console.log(), then send it back using res.json().
In order to get the product data back we need to actually parse the data. Express doesn’t parse request bodies for us. We are going to use the express recommended middleware.
Let’s add that to our server.js
server.js
const express = require('express');
const bodyParser = require('body-parser');
const api = require('./app');
const middleware = require('./middleware');
const port = process.env.PORT || 5000;const app = express();app.use(middleware.cors);
app.use(bodyParser.json());
app.get('/products', api.listProducts);
app.post('/products', api.createProduct);
app.put('/products/:id', api.editProduct);
app.delete('/products/:id', api.deleteProduct);
As you can see we also add the put and delete methods at the bottom. The put method in case you were wondering is another way to update an existing piece of data.
Let’s update the app.js file with the methods for put and delete.
app.js
async function editProduct(req, res, next) {
res.json(req.body);
}async function deleteProduct(req, res, next) {
res.json({success: true });
}
Summary
I hope that you enjoyed learning about Node and building out a simple yet powerful API that can be easily converted to an e-commerce API for just about any type of business. I had a lot of fun writing this post. If I see a huge interest in this I will most definitely write more content for it. Thanks for reading.