REST APIs allow web applications, mobile apps, desktop software, and third-party services to communicate with a server. Express.js makes REST API development with Node.js simple, flexible, and suitable for projects ranging from small applications to enterprise systems.
This tutorial assumes that you already understand the basics of Node.js and Express.js. New developers should first read Express.js Tutorial for Beginners: Build Your First Web Server.
What Is a REST API?
REST stands for Representational State Transfer. A REST API exposes resources through URLs and uses standard HTTP methods to perform operations on those resources.
For example, a product management API may use the following endpoints:
| HTTP Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/products |
Retrieve all products |
| GET | /api/products/:id |
Retrieve one product |
| POST | /api/products |
Create a new product |
| PUT | /api/products/:id |
Replace or fully update a product |
| PATCH | /api/products/:id |
Partially update a product |
| DELETE | /api/products/:id |
Delete a product |
Prerequisites
Before starting, make sure your computer has the following:
- Node.js installed
- npm installed
- A code editor such as Visual Studio Code
- Basic JavaScript knowledge
- Postman, Insomnia, or another API-testing tool
You can verify your Node.js and npm installations with:
node -v
npm -v
Developers who need to switch between Node.js versions can read How to Use NVM (Node Version Manager) Like a Pro.
Step 1: Create the Express.js Project
Open a terminal and create a new project directory:
mkdir express-rest-api
cd express-rest-api
Initialize the Node.js project:
npm init -y
This command creates a package.json file that stores project metadata, scripts, and dependencies.
Step 2: Install Express.js
npm install express
For development, install Nodemon so the application automatically restarts after code changes:
npm install --save-dev nodemon
Update the scripts section of package.json:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
Step 3: Create the Express Server
Create a file named server.js:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.get('/', (req, res) => {
res.json({
success: true,
message: 'Express REST API is running'
});
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
Start the development server:
npm run dev
Open the following address in your browser:
http://localhost:3000
Why express.json() Is Important
The following middleware allows Express.js to parse incoming JSON request bodies:
app.use(express.json());
Without it, req.body will usually be undefined when a client submits JSON data.
Step 4: Create Sample Product Data
For this beginner tutorial, we will store products in an array. Later, this array can be replaced with MySQL, PostgreSQL, MongoDB, or another database.
let products = [
{
id: 1,
name: 'Laptop',
price: 350,
stock: 10
},
{
id: 2,
name: 'Keyboard',
price: 15,
stock: 25
},
{
id: 3,
name: 'Mouse',
price: 8,
stock: 40
}
];
Step 5: Create a GET Endpoint for All Products
Add the following route below the products array:
app.get('/api/products', (req, res) => {
res.status(200).json({
success: true,
count: products.length,
data: products
});
});
Send a GET request to:
GET http://localhost:3000/api/products
The response will resemble:
{
"success": true,
"count": 3,
"data": [
{
"id": 1,
"name": "Laptop",
"price": 350,
"stock": 10
}
]
}
Step 6: Retrieve One Product by ID
app.get('/api/products/:id', (req, res) => {
const productId = Number(req.params.id);
const product = products.find(item => item.id === productId);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
return res.status(200).json({
success: true,
data: product
});
});
Test the route with:
GET http://localhost:3000/api/products/1
Why Number() Is Used
Route parameters are received as strings. Since product IDs are numeric, the value should be converted before comparing it:
const productId = Number(req.params.id);
Step 7: Create a Product with POST
app.post('/api/products', (req, res) => {
const { name, price, stock } = req.body;
if (!name || price === undefined || stock === undefined) {
return res.status(400).json({
success: false,
message: 'Name, price, and stock are required'
});
}
const parsedPrice = Number(price);
const parsedStock = Number(stock);
if (
!Number.isFinite(parsedPrice) ||
parsedPrice < 0 ||
!Number.isInteger(parsedStock) ||
parsedStock < 0
) {
return res.status(422).json({
success: false,
message: 'Price and stock must contain valid non-negative values'
});
}
const nextId = products.length
? Math.max(...products.map(product => product.id)) + 1
: 1;
const newProduct = {
id: nextId,
name: String(name).trim(),
price: parsedPrice,
stock: parsedStock
};
products.push(newProduct);
return res.status(201).json({
success: true,
message: 'Product created successfully',
data: newProduct
});
});
Send the following JSON request body:
{
"name": "Monitor",
"price": 85,
"stock": 12
}
Send it to:
POST http://localhost:3000/api/products
Step 8: Update a Product with PUT
A PUT request is commonly used to replace or fully update an existing resource.
app.put('/api/products/:id', (req, res) => {
const productId = Number(req.params.id);
const productIndex = products.findIndex(item => item.id === productId);
if (productIndex === -1) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
const { name, price, stock } = req.body;
if (!name || price === undefined || stock === undefined) {
return res.status(400).json({
success: false,
message: 'Name, price, and stock are required'
});
}
const parsedPrice = Number(price);
const parsedStock = Number(stock);
if (
!Number.isFinite(parsedPrice) ||
parsedPrice < 0 ||
!Number.isInteger(parsedStock) ||
parsedStock < 0
) {
return res.status(422).json({
success: false,
message: 'Invalid price or stock value'
});
}
const updatedProduct = {
id: productId,
name: String(name).trim(),
price: parsedPrice,
stock: parsedStock
};
products[productIndex] = updatedProduct;
return res.status(200).json({
success: true,
message: 'Product updated successfully',
data: updatedProduct
});
});
Example request:
PUT http://localhost:3000/api/products/1
{
"name": "Business Laptop",
"price": 425,
"stock": 8
}
Step 9: Partially Update a Product with PATCH
PATCH is useful when the client only needs to update selected fields.
app.patch('/api/products/:id', (req, res) => {
const productId = Number(req.params.id);
const product = products.find(item => item.id === productId);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
const allowedFields = ['name', 'price', 'stock'];
const submittedFields = Object.keys(req.body);
const invalidFields = submittedFields.filter(
field => !allowedFields.includes(field)
);
if (invalidFields.length > 0) {
return res.status(400).json({
success: false,
message: `Invalid fields: ${invalidFields.join(', ')}`
});
}
if (req.body.name !== undefined) {
const name = String(req.body.name).trim();
if (!name) {
return res.status(422).json({
success: false,
message: 'Name cannot be empty'
});
}
product.name = name;
}
if (req.body.price !== undefined) {
const price = Number(req.body.price);
if (!Number.isFinite(price) || price < 0) {
return res.status(422).json({
success: false,
message: 'Price must be a valid non-negative number'
});
}
product.price = price;
}
if (req.body.stock !== undefined) {
const stock = Number(req.body.stock);
if (!Number.isInteger(stock) || stock < 0) {
return res.status(422).json({
success: false,
message: 'Stock must be a non-negative integer'
});
}
product.stock = stock;
}
return res.status(200).json({
success: true,
message: 'Product updated successfully',
data: product
});
});
Example request:
PATCH http://localhost:3000/api/products/1
{
"stock": 20
}
PUT vs PATCH
| Method | Typical Purpose | Request Data |
|---|---|---|
| PUT | Replace or fully update a resource | Usually contains every required field |
| PATCH | Partially update a resource | Contains only fields that should change |
Step 10: Delete a Product
app.delete('/api/products/:id', (req, res) => {
const productId = Number(req.params.id);
const productIndex = products.findIndex(item => item.id === productId);
if (productIndex === -1) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
const deletedProduct = products[productIndex];
products.splice(productIndex, 1);
return res.status(200).json({
success: true,
message: 'Product deleted successfully',
data: deletedProduct
});
});
Test the endpoint:
DELETE http://localhost:3000/api/products/1
Add Search and Filtering
Query parameters can be used to filter products without creating separate routes.
app.get('/api/products', (req, res) => {
const search = String(req.query.search || '').trim().toLowerCase();
const minPrice = req.query.minPrice !== undefined
? Number(req.query.minPrice)
: null;
const maxPrice = req.query.maxPrice !== undefined
? Number(req.query.maxPrice)
: null;
let results = [...products];
if (search) {
results = results.filter(product =>
product.name.toLowerCase().includes(search)
);
}
if (minPrice !== null && Number.isFinite(minPrice)) {
results = results.filter(product => product.price >= minPrice);
}
if (maxPrice !== null && Number.isFinite(maxPrice)) {
results = results.filter(product => product.price <= maxPrice);
}
return res.status(200).json({
success: true,
count: results.length,
data: results
});
});
Example:
GET /api/products?search=lap&minPrice=100&maxPrice=500
Add Pagination
Pagination prevents an API from returning thousands of records in one response.
app.get('/api/products', (req, res) => {
const page = Math.max(Number.parseInt(req.query.page, 10) || 1, 1);
const limit = Math.min(
Math.max(Number.parseInt(req.query.limit, 10) || 10, 1),
100
);
const startIndex = (page - 1) * limit;
const paginatedProducts = products.slice(
startIndex,
startIndex + limit
);
return res.status(200).json({
success: true,
pagination: {
page,
limit,
total: products.length,
totalPages: Math.ceil(products.length / limit)
},
data: paginatedProducts
});
});
Example:
GET /api/products?page=2&limit=10
Use Proper HTTP Status Codes
| Status Code | Meaning | Common Usage |
|---|---|---|
200 |
OK | Successful GET, PUT, PATCH, or DELETE request |
201 |
Created | A new resource was created |
204 |
No Content | Successful request with no response body |
400 |
Bad Request | Malformed or incomplete input |
401 |
Unauthorized | Authentication is missing or invalid |
403 |
Forbidden | The authenticated user lacks permission |
404 |
Not Found | The requested resource does not exist |
409 |
Conflict | Duplicate or conflicting data |
422 |
Unprocessable Content | Validation failed |
429 |
Too Many Requests | Rate limit exceeded |
500 |
Internal Server Error | An unexpected server-side error occurred |
Add a 404 Handler
Place this middleware after all registered routes:
app.use((req, res) => {
res.status(404).json({
success: false,
message: 'API endpoint not found'
});
});
Add Centralized Error Handling
Error-handling middleware must contain four parameters:
app.use((err, req, res, next) => {
console.error(err);
const statusCode = Number.isInteger(err.statusCode)
? err.statusCode
: 500;
res.status(statusCode).json({
success: false,
message: statusCode === 500
? 'An unexpected server error occurred'
: err.message
});
});
Handle Asynchronous Route Errors
Database operations and external API calls are usually asynchronous. A reusable wrapper can forward rejected promises to the central error handler.
const asyncHandler = handler => {
return (req, res, next) => {
Promise.resolve(handler(req, res, next)).catch(next);
};
};
app.get('/api/reports', asyncHandler(async (req, res) => {
const reports = await loadReports();
res.status(200).json({
success: true,
data: reports
});
}));
Recommended REST API Response Format
Consistent responses make APIs easier for frontend and mobile developers to consume.
Successful Response
{
"success": true,
"message": "Product created successfully",
"data": {
"id": 4,
"name": "Monitor"
}
}
Error Response
{
"success": false,
"message": "Product not found",
"errors": []
}
Organize the API into Separate Files
Keeping every route inside server.js becomes difficult as an application grows.
A scalable structure may look like this:
express-rest-api/
│
├── src/
│ ├── config/
│ │ └── database.js
│ ├── controllers/
│ │ └── productController.js
│ ├── middleware/
│ │ ├── errorHandler.js
│ │ └── validateProduct.js
│ ├── models/
│ │ └── Product.js
│ ├── routes/
│ │ └── productRoutes.js
│ ├── services/
│ │ └── productService.js
│ └── app.js
│
├── tests/
├── .env
├── .gitignore
├── package.json
└── server.js
Create a Product Router
Create src/routes/productRoutes.js:
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({
success: true,
message: 'Get all products'
});
});
router.get('/:id', (req, res) => {
res.json({
success: true,
message: `Get product ${req.params.id}`
});
});
router.post('/', (req, res) => {
res.status(201).json({
success: true,
message: 'Create product'
});
});
router.put('/:id', (req, res) => {
res.json({
success: true,
message: `Replace product ${req.params.id}`
});
});
router.patch('/:id', (req, res) => {
res.json({
success: true,
message: `Update product ${req.params.id}`
});
});
router.delete('/:id', (req, res) => {
res.json({
success: true,
message: `Delete product ${req.params.id}`
});
});
module.exports = router;
Mount the router in the application:
const productRoutes = require('./src/routes/productRoutes');
app.use('/api/products', productRoutes);
Separate Controllers from Routes
Routes should define URLs, while controllers should handle request and response logic.
Create src/controllers/productController.js:
function getProducts(req, res) {
res.status(200).json({
success: true,
data: []
});
}
function createProduct(req, res) {
res.status(201).json({
success: true,
message: 'Product created',
data: req.body
});
}
module.exports = {
getProducts,
createProduct
};
Then update the router:
const express = require('express');
const {
getProducts,
createProduct
} = require('../controllers/productController');
const router = express.Router();
router.get('/', getProducts);
router.post('/', createProduct);
module.exports = router;
Use Environment Variables
Install the environment-variable package:
npm install dotenv
Create a .env file:
PORT=3000
NODE_ENV=development
DATABASE_URL=your_database_connection
JWT_SECRET=replace_with_a_strong_secret
Load it at the beginning of the application:
require('dotenv').config();
.env file to a public Git repository.
Create a .gitignore File
node_modules/
.env
npm-debug.log*
coverage/
dist/
REST API Security Essentials
A production API needs more than CRUD routes. It should also include appropriate security controls.
- Validate and normalize every request.
- Use authentication for private endpoints.
- Check authorization and user permissions.
- Use HTTPS in production.
- Limit request body sizes.
- Add rate limiting.
- Configure CORS carefully.
- Use parameterized database queries.
- Avoid returning confidential database fields.
- Record security-relevant activities in audit logs.
Install Common Security Middleware
npm install helmet cors express-rate-limit
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
app.use(helmet());
app.use(cors({
origin: ['https://example.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
credentials: true
}));
app.use(express.json({
limit: '1mb'
}));
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 100,
standardHeaders: true,
legacyHeaders: false
});
app.use('/api', apiLimiter);
API Versioning
API versioning allows you to introduce breaking changes without immediately affecting existing applications.
app.use('/api/v1/products', productRoutes);
A later version may use:
/api/v2/products
Testing the API with cURL
GET Products
curl http://localhost:3000/api/products
Create a Product
curl -X POST http://localhost:3000/api/products \
-H "Content-Type: application/json"
-d "{"name":"Monitor","price":85,"stock":12}"
Update a Product
curl -X PATCH http://localhost:3000/api/products/1 \
-H "Content-Type: application/json"
-d "{"stock":20}"
Delete a Product
curl -X DELETE http://localhost:3000/api/products/1
Common REST API Mistakes
REST API Best-Practices Checklist
- Use resource-based URL names such as
/products. - Use plural nouns consistently.
- Use the correct HTTP method.
- Return appropriate HTTP status codes.
- Validate every request.
- Use consistent response structures.
- Separate routes from controllers.
- Implement centralized error handling.
- Add filtering, sorting, and pagination.
- Use authentication and permission checks.
- Apply rate limits and request-size limits.
- Store secrets in environment variables.
- Use HTTPS in production.
- Write automated tests.
- Document endpoints and payloads.
Complete Beginner API Example
The following example combines the main concepts from this tutorial into a single file:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json({
limit: '1mb'
}));
let products = [
{
id: 1,
name: 'Laptop',
price: 350,
stock: 10
}
];
app.get('/api/products', (req, res) => {
res.status(200).json({
success: true,
count: products.length,
data: products
});
});
app.get('/api/products/:id', (req, res) => {
const productId = Number(req.params.id);
const product = products.find(item => item.id === productId);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
return res.status(200).json({
success: true,
data: product
});
});
app.post('/api/products', (req, res) => {
const { name, price, stock } = req.body;
if (!name || price === undefined || stock === undefined) {
return res.status(400).json({
success: false,
message: 'Name, price, and stock are required'
});
}
const parsedPrice = Number(price);
const parsedStock = Number(stock);
if (
!Number.isFinite(parsedPrice) ||
parsedPrice < 0 ||
!Number.isInteger(parsedStock) ||
parsedStock < 0
) {
return res.status(422).json({
success: false,
message: 'Invalid price or stock'
});
}
const nextId = products.length
? Math.max(...products.map(product => product.id)) + 1
: 1;
const product = {
id: nextId,
name: String(name).trim(),
price: parsedPrice,
stock: parsedStock
};
products.push(product);
return res.status(201).json({
success: true,
message: 'Product created successfully',
data: product
});
});
app.patch('/api/products/:id', (req, res) => {
const productId = Number(req.params.id);
const product = products.find(item => item.id === productId);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
if (req.body.name !== undefined) {
const name = String(req.body.name).trim();
if (!name) {
return res.status(422).json({
success: false,
message: 'Name cannot be empty'
});
}
product.name = name;
}
if (req.body.price !== undefined) {
const price = Number(req.body.price);
if (!Number.isFinite(price) || price < 0) {
return res.status(422).json({
success: false,
message: 'Invalid price'
});
}
product.price = price;
}
if (req.body.stock !== undefined) {
const stock = Number(req.body.stock);
if (!Number.isInteger(stock) || stock < 0) {
return res.status(422).json({
success: false,
message: 'Invalid stock'
});
}
product.stock = stock;
}
return res.status(200).json({
success: true,
message: 'Product updated successfully',
data: product
});
});
app.delete('/api/products/:id', (req, res) => {
const productId = Number(req.params.id);
const productIndex = products.findIndex(
item => item.id === productId
);
if (productIndex === -1) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
const [deletedProduct] = products.splice(productIndex, 1);
return res.status(200).json({
success: true,
message: 'Product deleted successfully',
data: deletedProduct
});
});
app.use((req, res) => {
res.status(404).json({
success: false,
message: 'API endpoint not found'
});
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
success: false,
message: 'An unexpected server error occurred'
});
});
app.listen(PORT, () => {
console.log(`API running at http://localhost:${PORT}`);
});
Frequently Asked Questions
Conclusion
Building a REST API with Express.js begins with understanding resources, routes, HTTP methods, request bodies, validation, and status codes. Once these fundamentals are in place, the API can be expanded with database integration, authentication, authorization, logging, documentation, and automated testing.
The in-memory product API created in this tutorial is suitable for learning. A real production application should store data in a database, separate responsibilities into modules, protect sensitive routes, validate all requests, and use centralized error handling.
About ShasTech-IT
ShasTech-IT develops REST APIs, Node.js applications, Express.js backends, ERP systems, HRMS platforms, warehouse management systems, POS applications, mobile apps, and custom business software.
- How to Install Node.js on Windows, Linux, and macOS
- What Is NPM? Complete Guide for Beginners
- How to Use NVM (Node Version Manager) Like a Pro
- How to Upgrade and Downgrade Node.js Safely
- Common Node.js Errors and How to Fix Them
- Express.js Tutorial for Beginners
- Express.js Routing Explained
- Express.js Middleware Explained
- Express.js Template Engines Explained: EJS vs Pug vs Handlebars