Node.js Building RESTful APIs
A REST API (Representational State Transfer Application Programming Interface) is a set of rules for building web services that allow different applications to communicate over HTTP. REST APIs are the backbone of modern web development — they allow a frontend (like a React app) to talk to a backend (Node.js server), or different backend services to exchange data.
Every action in a REST API — getting data, creating data, updating data, or deleting data — is performed using standard HTTP methods on clearly structured URLs called endpoints.
REST Principles
- Client-Server: The client (frontend) and server (backend) are separate. They communicate through requests and responses.
- Stateless: Each request contains all the information needed to process it. The server does not store session data between requests.
- Uniform Interface: Resources are accessed through consistent, predictable URLs.
- Resource-Based URLs: URLs represent resources (nouns) like
/users,/products— not actions.
HTTP Methods and Their CRUD Mapping
| HTTP Method | CRUD Operation | Example Endpoint | Description |
|---|---|---|---|
| GET | Read | GET /products | Retrieve all products |
| GET | Read | GET /products/:id | Retrieve a single product by ID |
| POST | Create | POST /products | Create a new product |
| PUT | Update (full) | PUT /products/:id | Replace all fields of a product |
| PATCH | Update (partial) | PATCH /products/:id | Update only specific fields |
| DELETE | Delete | DELETE /products/:id | Delete a product by ID |
Building a Complete RESTful API – Products Example
This example builds a full in-memory Products API with all CRUD operations. In a real application, the data would come from a database instead of an array.
Project Setup
mkdir products-api
cd products-api
npm init -y
npm install express
app.js – Full Products REST API
const express = require('express');
const app = express();
app.use(express.json()); // Parse incoming JSON bodies
// In-memory data store (replaces a real database for this example)
let products = [
{ id: 1, name: 'Laptop', price: 999, category: 'Electronics' },
{ id: 2, name: 'Desk Chair', price: 249, category: 'Furniture' },
{ id: 3, name: 'Coffee Mug', price: 15, category: 'Kitchen' }
];
let nextId = 4; // Tracks the next available ID
// ─────────────────────────────────────────────
// GET /api/products – Retrieve all products
// ─────────────────────────────────────────────
app.get('/api/products', function(req, res) {
res.json({
success: true,
count: products.length,
data: products
});
});
// ─────────────────────────────────────────────
// GET /api/products/:id – Retrieve one product
// ─────────────────────────────────────────────
app.get('/api/products/:id', function(req, res) {
const id = parseInt(req.params.id);
const product = products.find(p => p.id === id);
if (!product) {
return res.status(404).json({ success: false, error: 'Product not found' });
}
res.json({ success: true, data: product });
});
// ─────────────────────────────────────────────
// POST /api/products – Create a new product
// ─────────────────────────────────────────────
app.post('/api/products', function(req, res) {
const { name, price, category } = req.body;
if (!name || !price || !category) {
return res.status(400).json({
success: false,
error: 'name, price, and category are all required.'
});
}
const newProduct = {
id: nextId++,
name,
price: parseFloat(price),
category
};
products.push(newProduct);
res.status(201).json({ success: true, data: newProduct });
});
// ─────────────────────────────────────────────
// PUT /api/products/:id – Fully update a product
// ─────────────────────────────────────────────
app.put('/api/products/:id', function(req, res) {
const id = parseInt(req.params.id);
const index = products.findIndex(p => p.id === id);
if (index === -1) {
return res.status(404).json({ success: false, error: 'Product not found' });
}
const { name, price, category } = req.body;
if (!name || !price || !category) {
return res.status(400).json({
success: false,
error: 'All fields (name, price, category) are required for a full update.'
});
}
products[index] = { id, name, price: parseFloat(price), category };
res.json({ success: true, data: products[index] });
});
// ─────────────────────────────────────────────
// PATCH /api/products/:id – Partially update a product
// ─────────────────────────────────────────────
app.patch('/api/products/:id', function(req, res) {
const id = parseInt(req.params.id);
const product = products.find(p => p.id === id);
if (!product) {
return res.status(404).json({ success: false, error: 'Product not found' });
}
// Only update the fields that were sent
if (req.body.name !== undefined) product.name = req.body.name;
if (req.body.price !== undefined) product.price = parseFloat(req.body.price);
if (req.body.category !== undefined) product.category = req.body.category;
res.json({ success: true, data: product });
});
// ─────────────────────────────────────────────
// DELETE /api/products/:id – Delete a product
// ─────────────────────────────────────────────
app.delete('/api/products/:id', function(req, res) {
const id = parseInt(req.params.id);
const index = products.findIndex(p => p.id === id);
if (index === -1) {
return res.status(404).json({ success: false, error: 'Product not found' });
}
const deleted = products.splice(index, 1);
res.json({ success: true, message: 'Product deleted', data: deleted[0] });
});
// ─────────────────────────────────────────────
// 404 – Route not found handler
// ─────────────────────────────────────────────
app.use(function(req, res) {
res.status(404).json({ success: false, error: 'Endpoint not found' });
});
app.listen(3000, () => console.log('Products API running at http://localhost:3000'));
Testing the API with curl Commands
Get all products
curl http://localhost:3000/api/products
Get a single product
curl http://localhost:3000/api/products/1
Create a new product
curl -X POST http://localhost:3000/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Keyboard","price":89,"category":"Electronics"}'
Update a product
curl -X PATCH http://localhost:3000/api/products/1 \
-H "Content-Type: application/json" \
-d '{"price":1099}'
Delete a product
curl -X DELETE http://localhost:3000/api/products/3
Adding Query-Based Filtering
Filtering results using query parameters is a common REST API feature:
// GET /api/products?category=Electronics&maxPrice=500
app.get('/api/products', function(req, res) {
let result = [...products];
if (req.query.category) {
result = result.filter(p =>
p.category.toLowerCase() === req.query.category.toLowerCase()
);
}
if (req.query.maxPrice) {
result = result.filter(p => p.price <= parseFloat(req.query.maxPrice));
}
res.json({ success: true, count: result.length, data: result });
});
Consistent API Response Structure
A well-designed API always returns responses in a consistent format. The pattern used throughout this example is:
// Success response
{
"success": true,
"data": { ... }
}
// Error response
{
"success": false,
"error": "Description of what went wrong"
}
Consistency makes the API predictable and easy for frontend developers to consume.
Key Points
- A RESTful API uses HTTP methods (GET, POST, PUT, PATCH, DELETE) to perform CRUD operations on resources.
- URLs should represent resources (nouns), not actions — use
/products, not/getProducts. - Always validate incoming data and return meaningful error messages with the correct HTTP status code.
- Use
express.json()middleware to parse JSON request bodies. - Return consistent response structures across all endpoints for predictability.
- Query parameters enable flexible filtering, sorting, and pagination without changing the route.
- In a real application, in-memory arrays are replaced by database operations using tools like Mongoose (MongoDB) or Sequelize (SQL).
