This website uses cookies

Our website, platform and/or any sub domains use cookies to understand how you use our services, and to improve both your experience and our marketing relevance.

Manage client sites with AI. Join our free live webinar and see Cloudways MCP in action.Register now!

How to Build a REST API in Node.js

Updated on July 28, 2026

14 Min Read
rest api nodejs guide

Key Takeaways

  • REST API design principles like resource-based URLs, plural endpoints, and consistent error shapes make an API predictable and easy to consume.
  • The mini project builds a full CRUD product inventory API in Node.js and Express, backed by better-sqlite3.
  • Postman is used to test every endpoint locally before the code is pushed to GitHub.
  • The finished API deploys to Cloudways Managed Node.js Hosting and gets retested against the live URL.

Every Node.js project ends up needing an API sooner or later. Frontends need data. Mobile apps need something to talk to. Other services need endpoints to hit.

Building one isn’t complicated on paper. In practice though, it’s easy to get the small stuff wrong. Wrong status codes, inconsistent responses, endpoints that don’t follow conventions. The API technically works but feels sloppy the moment anyone tries to use it.

This blog covers what a REST API is, some design principles worth knowing upfront, and then walks through building a small product inventory API end to end. Plus deployment on Cloudways managed NodeJS hosting at the end.

What Is a REST API and Why Use Node.js

REST is basically a set of rules for building HTTP APIs. Nothing more mysterious than that.

Each resource (products, users, whatever) lives at its own URL. You use HTTP methods to interact with them:

  • GET reads
  • POST creates
  • PUT and PATCH update
  • DELETE removes

Every response comes back with a status code. 200-something means success. 400-something means the client sent a bad request. 500-something means the server tripped over itself. Details matter here, but that’s the general shape.

Node.js works well for this kind of thing. JavaScript on both sides means one language across the whole stack, which is convenient. It’s also good at handling lots of concurrent requests without slowing down (that’s the non-blocking I/O part, if you’ve heard the term).

Express is what most people reach for. Small, unopinionated, does one thing well. Pair it with a couple of middleware packages and you’ve got everything you need.

REST API Design Principles Worth Knowing

A few things worth getting right before touching any code. Easier to set up right the first time than to change later.

Endpoints are nouns. Not verbs.

/products is right. /getProducts is not. The HTTP method already tells you the action, so the URL just points at the thing.

Use plurals for collections. /products for the list, /products/:id for one specific product. Predictable is good.

For related data, nest the routes. /products/:id/reviews reads as “the reviews belonging to this product.” Clean.

Pick a naming convention and stick with it. kebab-case or camelCase, doesn’t really matter which. Just don’t mix them.

Errors need a consistent shape too. If half your endpoints return { error: “…” } and the other half return { message: “…”, code: 400 }, whoever’s using your API has to write different code for every endpoint. Not fun.

Mini Project: Building the Product Inventory API

For the mini project I’ll build a product inventory API. Add products, list them (filter by category), fetch by ID, update, delete. Basic CRUD, but with proper structure.

By the end, this runs locally, gets tested through Postman, and deploys to a live server.

Prerequisites

Before starting:

  • Node.js installed
  • A code editor (VS Code is fine)
  • Postman, Thunder Client, or curl for testing
  • Basic understanding of JavaScript and HTTP

What I’ll Be Using

  • Node.js
  • VS Code
  • Express, better-sqlite3, dotenv, cors, helmet
  • Postman for API testing

Step 1: Setting Up the Node.js Project

I need to make sure Node.js is up and running on my machine before I spin up any code.

Because I am working on an office laptop with locked-down IT restrictions, I can’t use the standard executable installer. Instead, I’ll grab the standalone Windows binary straight from nodejs.org. I unzipped the file contents and dropped them directly into my Downloads directory.

Node.js standalone binary download page for Windows

Extracted Node.js binary folder contents on the desktop

Opening the project folder in Command Prompt

Next up, I’ll head over to my desktop and create a brand new home directory for my project files named node-product-inventory-api.

New project folder named node-product-inventory-api on the desktop

Now I’ll fire up the Command Prompt console so I can navigate straight into that path:

cd C:\Users\abdulrehman\Desktop\node-product-inventory-api

Command Prompt navigated into the project folder path

Pointing Command Prompt to the Node binary

Since my machine didn’t use a formal system installer, Command Prompt doesn’t have a global reference for the node runtime environment yet. I need to explicitly link the path using the console tool:

set PATH=%PATH%;C:\Users\abdulrehman\Downloads\node-v24.18.0-win-x64\node-v24.18.0-win-x64

To quickly check if my terminal is able to find and use the Node files on my machine, I’ll run these two tests commands:

node -v
npm -v

Since both commands are returning the version info, I can move to the next step, which is to create the actual Node project.

Command Prompt showing Node.js and npm version output

Creating the Node.js project

The first step I need to do is initialize the project. I can do this by running the following command to generate a package.json file:

npm init -y

Generated package.json file after running npm init

With the package.json file created, I’ll pull all the specific external libraries I need to build the backend for my project, like:

  • Express for the server routing layer
  • Better-sqlite3 to handle local engine operations
  • Dotenv for handling configs
  • CORS to open access boundaries
  • Helmet to automatically inject missing security headers

Here’s the command I’ll run:

npm install express better-sqlite3 dotenv cors helmet

Terminal output after installing Express and other npm packages

Step 2: Setting Up the Database

Now that my Node project is created, I’ll open the project in VS Code. To do this, I’ll open VS Code > File > Open Folder and select my desktop node-product-inventory-api folder.

Opening the project folder in VS Code via the File menu

Now that the project’s opened in VS code, I can start building the mini project.

First, I’ll create a db folder in the project’s root, and inside it, a file called database.js. I’m doing this because I need a place to store the products I’ll interact with using the REST API.

In the new file I created, I’ll paste this code snippet:

const path = require('path');
const Database = require('better-sqlite3');
const db = new Database(path.join(__dirname, 'products.db'));
db.exec(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sku TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
description TEXT,
price REAL NOT NULL,
quantity INTEGER NOT NULL DEFAULT 0,
category TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
module.exports = db;

database.js file with SQLite table schema in VS Code

In the code snippet above, the UNIQUE constraint in the sku field makes sure every product has a unique SKU. The price and quantity fields are set to NOT NULL because they’re required for every product.

Step 3: Building the Server Base

Next, I’ll create a server.js file in the project root and add the following code:

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const db = require('./db/database');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(helmet());
app.use(cors());
app.use(express.json());
app.get('/', (req, res) => {
res.json({
status: 'ok',
message: 'Product Inventory API is running',
});
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

server.js file with Express app setup in VS Code

In the code snippet above, I’ve used Helmet to set common security headers. And also, CORS to allow requests from other origins.

I also used express.json() because I’ll be sending JSON data from Postman when testing my API endpoints.

The root route will work as a simple health check. After deployment, I can visit the base URL to verify that the API is running correctly.

Step 4: Building the CRUD Endpoints

Now, I’ll create the endpoints for the CRUD operation. And each endpoint I’ll create, I’ll add them to the server.js file, above the app.listen(…) line.

GET /products (with optional category filter)

app.get('/products', (req, res) => {
const { category } = req.query;
try {
let products;
if (category) {
products = db
.prepare(
'SELECT * FROM products WHERE category = ? ORDER BY created_at DESC'
)
.all(category);
} else {
products = db
.prepare('SELECT * FROM products ORDER BY created_at DESC')
.all();
}
res.json({ count: products.length, products });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to fetch products' });
}
});

In this code snippet, I’ve used the category query parameter. Totally optional, but if I include it (for example, /products?category=electronics), the API should return only matching products. Otherwise, it’ll return all products.

GET /products/:id

app.get('/products/:id', (req, res) => {
try {
const product = db
.prepare('SELECT * FROM products WHERE id = ?')
.get(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json({ product });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to fetch product' });
}
});

I use this endpoint to retrieve a product by its ID. If an ID doesn’t exist, the API will return a 404 response instead of an empty response.

POST /products

app.post('/products', (req, res) => {
const { sku, name, description, price, quantity, category } = req.body;
if (!sku || !name || price === undefined) {
return res
.status(400)
.json({ error: 'sku, name, and price are required' });
}
try {
const result = db
.prepare(`
INSERT INTO products
(sku, name, description, price, quantity, category)
VALUES
(?, ?, ?, ?, ?, ?)
`)
.run(
sku,
name,
description || null,
price,
quantity || 0,
category || null
);
const newProduct = db
.prepare('SELECT * FROM products WHERE id = ?')
.get(result.lastInsertRowid);
res.status(201).json({ product: newProduct });
} catch (err) {
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
return res
.status(400)
.json({ error: 'A product with that SKU already exists' });
}
console.error(err);
res.status(500).json({ error: 'Failed to create product' });
}
});

This endpoint will create a new product. I’ve included some basic validation to make sure the sku, name, and price fields are provided before the request reaches the database.

The code will also check for duplicate SKUs and return a 400 response in case it finds a duplicate.

If there are no errors, the API will return a 201 code, the standard response for a successful resource creation.

PUT /products/:id

app.put('/products/:id', (req, res) => {
const { sku, name, description, price, quantity, category } = req.body;
const existing = db
.prepare('SELECT * FROM products WHERE id = ?')
.get(req.params.id);
if (!existing) {
return res.status(404).json({ error: 'Product not found' });
}
try {
db.prepare(`
UPDATE products
SET
sku = ?,
name = ?,
description = ?,
price = ?,
quantity = ?,
category = ?
WHERE id = ?
`).run(
sku ?? existing.sku,
name ?? existing.name,
description ?? existing.description,
price ?? existing.price,
quantity ?? existing.quantity,
category ?? existing.category,
req.params.id
);
const updated = db
.prepare('SELECT * FROM products WHERE id = ?')
.get(req.params.id);
res.json({ product: updated });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to update product' });
}
});

This endpoint will update an existing product. I used the ?? to specify that any field that’s left out of the request, keeps its current value. This way, I can update only the fields I want to change instead of sending the entire product object.

DELETE /products/:id

app.delete('/products/:id', (req, res) => {
try {
const result = db
.prepare('DELETE FROM products WHERE id = ?')
.run(req.params.id);
if (result.changes === 0) {
return res.status(404).json({ error: 'Product not found' });
}
res.json({ message: 'Product deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to delete product' });
}
});

This endpoint will delete a product by its ID. If an ID doesn’t exist, it returns a 404 response. Otherwise, it’ll return a success message confirming the product was deleted.

Step 5: Adding Global Error Handling

I’ve already added error handling in each endpoint, but just to be safe, I’ll also add a global fallback for anything I may have missed. So, I’ll add the following code to the bottom of server.js file, right before app.listen(…):

app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Something went wrong on our end' });
});

The first middleware will return a 404 Not Found response for any route that isn’t defined. The second will catch unexpected errors that aren’t handled elsewhere.

Step 6: Testing the API with Postman

Time to actually test this. I’ll add a start script to my package.json so I don’t have to keep typing the full node command:

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

package.json file with a start script added

Then, I’ll go back to my Command Prompt and run:

npm start

Terminal showing the Express server running on port 3000

Now my server is running on port 3000. To test each endpoint I created earlier, I’ll use Postman.

Create a product — POST to http://localhost:3000/products with a JSON body:

{
"sku": "LAPTOP-001",
"name": "Silver Laptop",
"description": "A 14-inch aluminum laptop",
"price": 1299.99,
"quantity": 10,
"category": "electronics"
}

If the request is successful, I’ll see a 201 response, the newly created product, including its auto-generated ID.

Postman response showing a newly created product with 201 status

Next, I’ll send a GET request to http://localhost:3000/products to retrieve all products.

Postman GET request returning the full list of products

If I add a query parameter like ?category=electronics, the API will return only products in that category.

Postman GET request filtered by category=electronics

I can then change the parameter to ?category=Portable to fetch a product from a different category.

Postman GET request filtered by category=Portable

Next, I’ll test fetching a single product with a GET request to http://localhost:3000/products/1.

Postman GET request fetching a single product by ID

If I try an ID that doesn’t exist, such as /products/999, the API will return a 404 response.

Postman 404 response for a product ID that doesn't exist

To test the Update endpoint, I’ll send a PUT request at http://localhost:3000/products/1 with only the price field I want to change. For example, { “price”: 1199.99 }.

Now, only the price is updated, and the rest of the fields stay the same.

Postman PUT request updating a product's price field

Finally, I’ll send the DELETE request at http://localhost:3000/products/1 to remove the product with ID “1”.

Postman DELETE request removing a product by ID

If I try to fetch the deleted product, I’ll get a 404 response, confirming that it was deleted earlier.

Postman 404 response confirming the product was deleted

Phew! All of the CRUD endpoints are working as expected.

API Working Locally? Now Take It Live

Once your endpoints pass testing in Postman, deploying them shouldn’t be the hard part.

Step 7: Making It Deployment-Ready

Before pushing my project to the Cloudways managed Node.js hosting, I’ll make a few small changes to get it ready for deployment.

First, I’ll create a .env file in the project root and paste this code in it:

NODE_ENV=development

.env file with the NODE_ENV variable in VS Code

There’s nothing sensitive stored in this file just yet, but I like setting it up early. When I deploy the app, I’ll change development to production.

I’ll also create a .gitignore file and paste this code in it:

node_modules
.env
db/products.db

.gitignore file excluding node_modules, .env, and the database

I excluded the database file because the production server should start with a fresh database, not the sample data I’ve been using in local production.

Step 8: Pushing to GitHub

With everything ready, I’ll push the project to GitHub. First, I’ll create a new repository named node-product-inventory-api.

Then, I’ll open Command Prompt and run the commands below one at a time:

git init
git add .
git commit -m "First commit"
git branch -M main
git remote add origin https://github.com/abdulrehman293/node-product-inventory-api
git push -u origin main

Once the push is complete, I’ll refresh the repository on GitHub. And now I can see all of the project files, with .env, node_modules, and the local database file excluded.

GitHub repository showing the pushed Node.js project files

Step 9: Deploying to Cloudways

With the project pushed to GitHub, I’m ready to deploy it. I’ll use Cloudways Managed Node.js Hosting for the deployment.

In the Cloudways dashboard, I’ll click Node.js from the left menu, then hit Launch Now.

Cloudways dashboard Node.js hosting launch screen

The Starter plan is fine for something this size.

Cloudways Starter plan selection for Node.js hosting

On the next screen, I’ll click Connect Via Git, sign into GitHub.

Once connected, I’ll pick my node-product-inventory-api repo and click continue.

Cloudways screen for connecting a GitHub repository

Now I’ll choose the config framework preset as Express. The Node Version as Node 24 (LTS). And leave the Root Directory as default.

Cloudways configuration screen with Express framework and Node 24

Next, I’ll click Change under Build and Output Settings, set the Package Manager to npm, and leave the Entry File set to server.js.

Cloudways build settings with npm package manager and entry file

Now, I’ll click on Add next to Environment Variables and add NODE_ENV to the key and production to the value.

Cloudways screen with the Deploy Now button for the Node.js app

Cloudways deployment progress log after clicking Deploy Now

After saving the changes, I’ll click Deploy Now. Cloudways will start pulling the latest code from GitHub and install the project dependencies.

Cloudways deployment log showing dependencies installing successfully

Moment of Truth

Once the deployment is successful, I’ll copy the temporary Cloudways URL from the app’s Overview page and go back to Postman.

Cloudways app Overview page showing the temporary deployment URL

Cloudways deployment success confirmation with the live app URL

Previously I used the localhost URL but now I’ll swap it with the Cloudways temporary URL to test each endpoint.

GET

First I’ll run the GET request to the base URL. If everything is set up properly, I’ll get back “Product Inventory API is running”, which confirms the API is alive.

Postman GET request to the live Cloudways URL confirming the API is running

Then I’ll test the rest of the other endpoints. Create a product. List them. Fetch by ID. Update. Delete. Same as what I did on local, just against the live URL this time.

POST

Postman POST request creating a product on the live Cloudways API

Postman response showing the newly created product on the live API

PUT

Postman PUT request updating a product on the live Cloudways API

DELETE

Postman DELETE request removing a product on the live Cloudways API

Now. if I fetch the deleted product, it’ll return a 404 status.

Postman 404 response confirming the product was deleted on production

Wrapping Up

So this wraps up our guide on building a REST API with Node.js. In this guide, I went over what REST is, covered some design principles worth knowing, and then built a product inventory API with full CRUD endpoints, validation, and error handling.

I also walked through testing everything locally with Postman and then deployed the project on a live Cloudways server.

I’ve pushed the finished project on my GitHub, so you guys can clone and reuse the code. And if you want to try deploying something similar yourself, our Managed Node.js Hosting handles this workflow seamlessly: connect your repo, pick a framework preset, and publish your app.

Deploy Your Own REST API on Cloudways

Connect your repo, pick a framework preset, and publish your Node.js app in minutes.

Q. What’s the difference between REST and GraphQL?

REST uses fixed endpoints, where each URL represents a resource and HTTP methods define the action to perform.

GraphQL typically uses a single endpoint, allowing clients to request exactly the data they need in a single query.

REST is generally easier to learn and widely supported, while GraphQL offers more flexibility for complex applications but comes with a steeper learning curve.

Q. Do I need Express to build a REST API in Node.js?

No. You can build a REST API using Node.js’s built-in http module. However, you’ll need to handle routing, middleware, and request parsing yourself.

Express simplifies these tasks, which is why it’s the most popular framework for building REST APIs in Node.js. If you’re looking for alternatives, Fastify and Koa are also worth exploring.

Q. How do I test my REST API without a frontend?

Tools like Postman and Thunder Client let you send GET, POST, PUT, PATCH, and DELETE requests without building a frontend. Thunder Client runs directly inside VS Code, making it a convenient option for many developers.

If you prefer the command line, you can also use cURL, although it’s less convenient for more complex requests.

Q. What’s the difference between PUT and PATCH?

PUT is intended to replace an entire resource, so the request typically includes every field, even those that haven’t changed.

PATCH is designed for partial updates, allowing you to send only the fields you want to modify.

In practice, many APIs. including the one built in this tutorial, use PUT for partial updates by leaving unchanged fields untouched.

Share your opinion in the comment section. COMMENT NOW

Share This Article

Start Growing with Cloudways Today.

Our Clients Love us because we never compromise on these

Abdul Rehman

Abdul is a tech-savvy, coffee-fueled, and creatively driven marketer who loves keeping up with the latest software updates and tech gadgets. He's also a skilled technical writer who can explain complex concepts simply for a broad audience. Abdul enjoys sharing his knowledge of the Cloud industry through user manuals, documentation, and blog posts.

×

Webinar: How to Get 100% Scores on Core Web Vitals

Join Joe Williams & Aleksandar Savkovic on 29th of March, 2021.

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Want to Experience the Cloudways Platform in Its Full Glory?

Take a FREE guided tour of Cloudways and see for yourself how easily you can manage your server & apps on the leading cloud-hosting platform.

Start my tour