Middleware Patterns Project — Build a Complete Middleware Pipeline
In this tutorial, you will learn about Middleware Patterns Project. We cover key concepts, practical examples, and best practices to help you master this topic.
This middleware patterns project guides you through building a production-ready Express API with a complete middleware pipeline combining logging, authentication, validation, Rate Limiting, error handling, and compression.
What You'll Learn
By completing this project, you will integrate all middleware patterns into a single application, handle real-world edge cases, and understand how middleware layers work together.
Why It Matters
Individual middleware tutorials teach isolated concepts. This project shows how they compose into a working system, just like DodaTech's production APIs.
Real-World Use
The API you build mirrors DodaZIP's file management API: authenticated users upload files, view their files, and manage their account, all protected by a layered middleware pipeline.
Project Learning Path
flowchart LR
A[Security Middleware] --> B[Project]
B --> C[Middleware Pipeline]
C --> D[Complete API]
B --> E{You Are Here}
style E fill:#f90,color:#fff
Project Overview
Build a file management API with the following middleware layers in order:
- Request logging (morgan)
- Security headers (helmet)
- CORS configuration
- Body Parsing with size limits
- Compression
- Rate limiting
- Authentication (JWT)
- Request validation
- Error handling
Step 1: Setup and Dependencies
npm init -y
npm install express helmet morgan cors compression express-rate-limit jsonwebtoken joi
npm install -D nodemon
const express = require("express");
const helmet = require("helmet");
const morgan = require("morgan");
const cors = require("cors");
const compression = require("compression");
const rateLimit = require("express-rate-limit");
const jwt = require("jsonwebtoken");
const Joi = require("joi");
const app = express();
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || "project-secret-key";
Step 2: Global Middleware Pipeline
app.use(helmet());
app.use(morgan("combined"));
app.use(cors({ origin: process.env.FRONTEND_URL || "http://localhost:5173" }));
app.use(compression());
app.use(express.json({ limit: "1mb" }));
const limiter = rateLimit({
windowMs: 60000,
max: 60,
message: { error: "Rate limit exceeded" }
});
app.use(limiter);
Expected behavior: Every request passes through security headers, logging, CORS, compression, body parsing, and rate limiting before reaching any route.
Step 3: Authentication Middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: "Authorization header required" });
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = { id: decoded.userId, username: decoded.username };
next();
} catch (err) {
return res.status(401).json({ error: "Invalid or expired token" });
}
}
Step 4: Validation Middleware
const fileSchema = Joi.object({
filename: Joi.string().min(1).max(255).required(),
content: Joi.string().max(10485760).allow("").optional(),
isPublic: Joi.boolean().default(false)
});
function validateFile(req, res, next) {
const { error, value } = fileSchema.validate(req.body);
if (error) {
return res.status(400).json({
error: "Validation failed",
details: error.details.map(d => d.message)
});
}
req.body = value;
next();
}
Step 5: Routes
app.post("/auth/login", (req, res) => {
const { username, password } = req.body;
if (username === "admin" && password === "secret") {
const token = jwt.sign(
{ userId: 1, username: "admin" },
JWT_SECRET,
{ expiresIn: "1h" }
);
return res.json({ token });
}
res.status(401).json({ error: "Invalid credentials" });
});
const files = [];
let fileId = 1;
app.get("/api/files", authenticate, (req, res) => {
const userFiles = files.filter(f => f.userId === req.user.id);
res.json({ files: userFiles });
});
app.post("/api/files", authenticate, validateFile, (req, res) => {
const file = {
id: fileId++,
userId: req.user.id,
filename: req.body.filename,
content: req.body.content,
isPublic: req.body.isPublic,
createdAt: new Date().toISOString()
};
files.push(file);
res.status(201).json({ file });
});
app.get("/api/files/:id", authenticate, (req, res) => {
const file = files.find(f => f.id === Number(req.params.id));
if (!file) return res.status(404).json({ error: "File not found" });
if (file.userId !== req.user.id && !file.isPublic) {
return res.status(403).json({ error: "Access denied" });
}
res.json({ file });
});
Step 6: Error Handling
app.use((req, res) => {
res.status(404).json({ error: "Route not found" });
});
app.use((err, req, res, next) => {
console.error("Unhandled error:", err.message);
res.status(500).json({
error: process.env.NODE_ENV === "production"
? "Internal server error"
: err.message
});
});
app.listen(PORT, () => {
console.log(`File API running on port ${PORT}`);
});
Step 7: Test the API
# Login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}'
Expected output:
{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
# Create file (with token)
curl -X POST http://localhost:3000/api/files \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"filename":"notes.txt","content":"Hello World"}'
Expected output:
{"file": {"id": 1, "userId": 1, "filename": "notes.txt", "content": "Hello World", "isPublic": false, "createdAt": "2026-06-28T10:30:00.000Z"}}
Common Mistakes
Middleware order — Authentication must come after body parsing. Rate limiting must come before authentication to block unauthenticated attacks.
Not validating token expiry — Expired tokens pass JWT signature verification. Always check the
expclaim.Exposing internal IDs — Use UUIDs instead of sequential IDs for file identifiers to prevent enumeration.
Not limiting file content size — A 1GB file content in the body exhausts memory. Always limit body size.
Storing passwords in plain text — This example uses plain text for simplicity. Production applications must hash passwords.
Practice Questions
Why is rate limiting placed before authentication in the pipeline? To block excessive requests before they reach the authentication middleware, preventing DoS attacks on the auth system.
How would you add file type validation to this API? Add a validation middleware that checks the filename extension against an allowed list.
What changes would you make for a distributed deployment? Replace in-memory rate limiting with Redis, use a database for file storage, and use a shared cache for tokens.
Challenge: Add a middleware that logs all file access attempts for auditing.
FAQ
Project Extension Ideas
- Add file sharing between users with permission middleware
- Implement file versioning with history middleware
- Add Webhook notifications using middleware events
- Implement request/response logging to a database
- Add API key authentication alongside JWT
What's Next
Congratulations on completing the middleware patterns project! Explore rate limiting patterns in depth. Then learn about caching strategies for backend APIs.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro