3. Express.js - Basic To Advance
Cors , Custom Middleware ,Error Handlers, Api Versing , Rate Limiting
What is Express.js ?
Express.js is a minimal & flexible web application framework built on top of Node.js.
It simplifies the process of building web servers, APIs, and backend applications.
Instead of writing raw
httpserver code in Node.js, Express provides an easier syntax, routing, and middleware system.
Installation and Setup
npm init -y # Initialize project npm install expressCreate Server from express.js
//in index.js -> main file const express = require('express'); // 1. get the express module const app = express(); // 2. invoke method const PORT = 3000; app.get('/', (req, res) => { // default raout res.send('Hello from Express.js!'); }); app.listen(PORT, () => { // listen server console.log(`Server running at http://localhost:${PORT}`); }); // This is app gives many methods like - app.post() , app.get() , app.Router(express.Router), app.use()Routing
//Express provides methods for handling different HTTP methods:
app.get('/user', (req, res) => res.send('GET request'));
app.post('/user', (req, res) => res.send('POST request'));
app.put('/user', (req, res) => res.send('PUT request'));
app.delete('/user', (req, res) => res.send('DELETE request'));
app.get('/user/:id', (req, res) => { // we pass id in the req params
res.send(`User ID is ${req.params.id}`);
});
Middlewares
Middleware functions are functions that run before the request is sent to the final route handler.
Types:
Application-level (for entire app)
Router-level
Built-in (like
express.json())Error-handling
//import express module first // Built-in middleware app.use(express.json()); // Custom middleware app.use((req, res, next) => { // we also make this callback function outside(best practice) console.log(`Request URL: ${req.url}`); next(); // Move to next middleware/route }); // Now on evry request my custom middleware runs cuz it is for entire app // Router level: that runs only for particular route - like auth middleware
Request & Response
req.body→ data from POST/PUTreq.params→ route parametersreq.query→ query stringres.send(),res.json(),res.status()
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
const express = require('express');
const app = express();
app.use(express.json());
let users = [{ id: 1, name: 'Ayush' }];
app.get('/users', (req, res) => res.json(users));
app.post('/users', (req, res) => {
users.push(req.body);
res.status(201).json(req.body);
});
app.listen(3000, () => console.log('API running on port 3000'));
Express with Template Engines
Supports EJS, Pug, Handlebars for server-side rendering.
EJS - Emmeded Javascript(EJS) is a simple templating language that lets you generate HTML markup with plain Javascript. It can help you to generate dynimic HTML pages in express application.
app.set('view engine', 'ejs');
app.get('/', (req, res) => res.render('index', { name: 'Ayush' }));
Real-World Uses of Express
REST APIs
GraphQL API layer
Authentication servers
Microservices
Full-stack apps (with React, Angular, Vue)
Middleware APIs (proxy, logging, monitoring)
Alternative to express
Fastify → Faster, similar syntax.
Koa → From same team, middleware-centric.
NestJS → Full framework, TypeScript-first.
EXPRESS.JS INTERMEDIATE CONCEPTS
CORS
CORS (Cross-Origin Resource Sharing) is a browser security policy that controls whether a web page at Origin A (scheme + host + port, e.g.
https://app.example.com:443) is allowed to read responses from Origin B (e.g.http://api.localhost:3000).Same origin? No CORS involved.
Cross origin? Browser enforces CORS:
CORS is not auth. It only decides if the browser can read the response.
const cors = require('cors')
const configureCore = ()=>{
return cors({
// origin: controls which origins (domains) are allowed to access your API
// It can be a string, array, or a function for dynamic checks.
origin:(origin,callback)=>{
const allowedOrigin =[
'http://localhost:3000', // local development frontend
'https://yourcustomdomain.com' // production domain frontend
]
if(!origin || allowedOrigin.indexOf(origin) !==-1){
callback(null , true); // giving permission so that request can be allowed
}
else{
callback(new Error("Not allowed by cors")) //reject request (browser blocks)
}
},
//methods -> which http methods you are allowing user to do the request
methods: ['GET' , 'POST' , 'PUT' , 'DELETE'],
// allowedHeaders: request headers the client is permitted to send
// Example: if frontend sends Authorization header, it must be declared here
allowedHeaders: [
'Content-Type', // required for JSON body requests
'Authorization',//// needed if using JWT/bearer tokens
'Accept-Version'
],
// exposedHeaders: response headers that the browser is allowed to read
// Without this, custom headers (like X-Total-Count) are invisible to JS on frontend
exposedHeaders:['X-Total-Count' , 'Content-Range'],
credentials: true,// enales support for cookies and authoraiation geaders -> imp one
// preflightContinue: if false, Express automatically ends OPTIONS preflight request
// If true, you must handle OPTIONS response yourself
preflightContinue:false,
maxAge:600, //cache pre flight responses for 10mins(600 sec) -> help us to avoid sending options request multiple times
optionsSuccessStatus:204 // provide status code to use sucessful options request , Some old browsers expect 200, default is 204 (No Content)
})
}
module.exports = configureCore;
// use this in our main index.js
app.use(configureCore())
What happens if we don’t use CORS middleware at all?
If your server doesn’t respond with the proper CORS headers (
Access-Control-Allow-Origin, etc.), the browser blocks the frontend JS from reading the response.The request reaches your backend and executes normally, but the frontend cannot access the response (it’s dropped at the browser level).
Example: In Chrome DevTools you’ll see a CORS error like:
Access to fetch at 'http://api.localhost:3000' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header present.
What happens if we allow all origins (origin: '*')?
Any website can make requests to your API from a browser.
✅ Good for public APIs (e.g. weather data, open maps, RSS feeds).
❌ Bad for APIs with sensitive data because:
- Anyone can embed your API in their website and trick users into making requests.
┌─────────────────────────────────────────────────────────────┐
│ Browser (Frontend: http://localhost:3000) │
└─────────────────────────────────────────────────────────────┘
│
│ 1. User makes a fetch() call:
│ fetch("http://api.localhost:4000/data", {
│ method: "POST",
│ headers: { "Content-Type": "application/json" }
│ })
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Browser CORS Engine (Security) │
└─────────────────────────────────────────────────────────────┘
│
│ 2. Sees it's a cross-origin request
│ (http://localhost:3000 → http://localhost:4000)
│
│ 3. Since method=POST with JSON → NOT "simple"
│ → Trigger **preflight OPTIONS** request
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Server (API: http://localhost:4000) │
└─────────────────────────────────────────────────────────────┘
│
│ 4. Browser sends:
│ OPTIONS /data
│ Origin: http://localhost:3000
│ Access-Control-Request-Method: POST
│ Access-Control-Request-Headers: content-type
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Server CORS Middleware (Express) │
└─────────────────────────────────────────────────────────────┘
│
│ 5. Server checks origin against allowlist
│ → If allowed, replies:
│ Access-Control-Allow-Origin: http://localhost:3000
│ Access-Control-Allow-Methods: GET,POST,PUT,DELETE
│ Access-Control-Allow-Headers: content-type
│ Access-Control-Allow-Credentials: true
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Browser CORS Engine (Security) │
└─────────────────────────────────────────────────────────────┘
│
│ 6. Browser inspects response headers
│ → If allowed → continues
│ → If missing headers → BLOCKS request
│
│ 7. Sends the **actual POST request**:
│ POST /data
│ Origin: http://localhost:3000
│ Content-Type: application/json
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Server API │
└─────────────────────────────────────────────────────────────┘
│
│ 8. Server responds with data + CORS headers:
│ HTTP/1.1 200 OK
│ Access-Control-Allow-Origin: http://localhost:3000
│ Content-Type: application/json
│ { "message": "success" }
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Browser JavaScript (Frontend App) │
└─────────────────────────────────────────────────────────────┘
│
│ 9. Browser delivers the JSON response
│ to the fetch() promise
│
▼
✅ SUCCESS → frontend can use the data
const requestLogger = (req,res, next)=>{
const timeStamp = new Date().toISOString();
const method = req.method;
const url = req.url
const userAgent = req.get("User-Agent")
console.log(timeStamp , method , url ,userAgent) // On every api hit in post man then this will print in the our console
next();
}
const addTimeStamp = (req,res,next)=>{
req.timeStamp = new Date().toISOString();
next();
}
module.exports = {requestLogger , addTimeStamp}
// In server.js(main file)
// these middle called on every request cuz they decalre as global level
app.use(requestLogger)
app.use(addTimeStamp)
app.use(configureCore())
app.use(express.json())
When to use custom middlewares ?
Authentication/Authorization
Logging
Validation
Error handling
Rate limiting
Data transformation
Custom headers
Error Handler Middleware -:
Request → Route runs inside
asyncHandler.If error →
next(err)forwards toglobalErrorHandler.globalErrorHandlerdecides:Custom APIError → use its statusCode & message.
ValidationError → return 400.
Else → return 500.
// custom error class
//By creating APIError, we can attach statusCode and handle API-specific errors properly.
class APIError extends Error {
constructor(message , statusCode){
super(message) // call to parent class
this.statusCode = statusCode; //// Custom property to store HTTP status (e.g., 400, 404, 500)
this.name = "APIError" // Set custom error name (helps in distinguishing error types)
}
}
const asyncHandler = (fn) => (req,res , next)=>{
// Wraps the async function and ensures any rejected Promise is passed to "next()"
// Without this, you'd need try/catch in every route
// With asyncHandler, any thrown error → .catch(next) → forwarded to global error handler.
Promise.resolve(fn(req,res,next)).catch(next)
}
//A global error handler ensures all errors are caught and consistent responses are returned.
const globalErrorHandler = (err , req , res, next)=>{
console.log(err.stack)
if(err instanceof APIError){
return res.status(err.statusCode).json({
status:"Error",
message: err.message
})
}
//also handle mongoose validation
else if (err.name === 'validationError'){
return res.status(400).json({
status:'error',
message:"validation error"
})
}
// Other unknown erros
else{
return res.status(500).json({
status:'error',
message:"Internal Server error"
})
}
}
module.exports = {APIError , asyncHandler , globalErrorHandler}
In Main Server.js
//--- other code ---
app.use(express.json())
// make dummy route
app.get("/user/:id", asyncHandler(async (req, res) => {
const userId = req.params.id;
if(userId !== "1"){
// Throw custom error if user not found
throw new APIError("User not found", 404);
}
res.json({ id: 1, name: "Ayush" });
}));
// Always place the error handler LAST
app.use(globalErrorHandler)
API Versing
APi Versing help us whether the particular API URL is actually starting with a particular version or not like v1 , v2
API versioning is a way to maintain multiple versions of your API simultaneously.
It helps when:You want to release new features without breaking old clients.
You need to deprecate old endpoints gradually.
Different clients (web, mobile, third-party apps) depend on different API behaviors.
Ways to do API Versing:
// 🔹 Way 1: Path/URL-based versioning
// Middleware factory → returns a middleware that checks if request path starts with /api/{version}
const apiVersioning = (version) => (req, res, next) => {
// Example: version = 'v1' → check if path starts with /api/v1
if (req.path.startsWith(`/api/${version}`)) {
next(); // ✅ allow request to continue
} else {
// ❌ version not supported → send error response
res.status(404).json({
success: false,
error: "API Version is not supported"
})
}
}
//in main server.js
app.use(apiVersioning('v1')); //// now only requests like /api/v1/users will pass
// 🔹 Way 2: Header-based versioning
// Middleware factory → checks if request contains "Accept-Version" header with correct version
const headerVersioning = (version) => (req, res, next) => {
// Example: Accept-Version: v1
if (req.get('Accept-Version') === version) {
next(); // ✅ correct version → allow
} else {
res.status(404).json({
success: false,
error: "API Version is not supported"
})
}
}
// Server.js
app.use("/api/users", headerVersioning("v2"), userRoutes);
// client must send → headers: { "Accept-Version": "v2" }
// 🔹 Way 3: Content-Type negotiation based versioning
// Middleware factory → checks if Content-Type header matches custom media type pattern
const contentTypeVersioning = (version) => (req, res, next) => {
const contentType = req.get('Content-Type');
// Example: Content-Type: application/vnd.api.v1+json
if (contentType && contentType.includes(`application/vnd.api.${version}+json`)) {
next(); // ✅ correct version → allow
} else {
res.status(404).json({
success: false,
error: "API Version is not supported"
})
}
}
// Server.js
app.use("/api/users", contentTypeVersioning("v1"), userRoutes);
// client must send header → Content-Type: application/vnd.api.v1+json
Rate Limiting
Rate limiting is a way to control how many requests a client can make to your API in a given time window.
It helps to:✅ Protect your server from DDoS attacks or brute force attacks
✅ Prevent abuse of APIs (e.g., login spamming)
✅ Ensure fair usage among users
✅ Reduce server load
npm install express-rate-limit
Make custom middleware - rateLimiting.js
//ratelimiting.js
const rateLimit = require('express-rate-limit')
const createBasicRateLimiter = (maxRequets , time)=>{
return rateLimit({
max:maxRequets,
windowMs:time,
message:"Too many requests, please try again later",
standardHeaders:true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders:false, // Disable the `X-RateLimit-*` headers
})
}
module.exports = createBasicRateLimiter;
//In server.js
app.use(createBasicRateLimiter(100 , 15*60*1000)) // 100 req per 15 minutes
app.use(express.json())
Final Server.js look like this
const express = require('express')
const app = express();
require('dotenv').config();
const cors = require('cors');
const configureCore = require('./config/corsConfig');
const { requestLogger, addTimeStamp } = require('./middlewares/customMiddleware');
const { globalErrorHandler, asyncHandler } = require('./middlewares/errorHandler');
const apiVersioning = require('./middlewares/apiVersioning');
const createBasicRateLimiter = require('./middlewares/rateLimiting');
const PORT = process.env.PORT ||5000
const itemsRoutes = require('./routes/item-routes')
// these middle called on every request cuz they decalre as global level
app.use(requestLogger)
app.use(addTimeStamp)
app.use(configureCore())
app.use(createBasicRateLimiter(100 , 15*60*1000)) // 100 req per 15 minutes
app.use(express.json())
// Apply versioning middleware FIRST
app.use(apiVersioning('v1'));
app.use('/api/v1', itemsRoutes) //http://localhost:3000/api/v2/items -> when you call this then version error came
// make dummy route
app.get("/user/:id", asyncHandler(async (req, res) => {
const userId = req.params.id;
if(userId !== "1"){
// Throw custom error if user not found
throw new APIError("User not found", 404);
}
res.json({ id: 1, name: "Ayush" });
}));
// Always place the error handler LAST
app.use(globalErrorHandler)
app.listen(PORT , ()=>{
console.log("Server is running at PORT" , PORT);
})
Comments
Join the discussionNo comments yet. Be the first to comment.