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 API Design Principles Worth Knowing
- Mini Project: Building the Product Inventory API
- Prerequisites
- What I’ll Be Using
- Step 1: Setting Up the Node.js Project
- Step 2: Setting Up the Database
- Step 3: Building the Server Base
- Step 4: Building the CRUD Endpoints
- Step 5: Adding Global Error Handling
- Step 6: Testing the API with Postman
- Step 7: Making It Deployment-Ready
- Step 8: Pushing to GitHub
- Step 9: Deploying to Cloudways
- Moment of Truth
- Wrapping Up
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.


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.

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

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.

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

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

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.

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;

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}`);
});

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"
}

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

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.

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

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

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

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

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

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.

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

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

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

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

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.

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.

The Starter plan is fine for something this size.

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.

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

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.

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


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

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.


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.

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


PUT

DELETE

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

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.
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.