
Creating Your Own ExpressJS from Scratch (Part 2) - Middlewares and Controllers
In Part 1, we laid the foundation of our custom Node.js web framework by implementing a basic routing system. In this part, we’ll build on that by adding support for middlewares and controllers — two fundamental building blocks in any modern web framework.
🧪 What You Will Learn
- Understand the middleware pattern and how it applies to route handling
- Chain multiple functions (middlewares and final controller) for each route
- Enhance your routing system to execute handlers sequentially
🧱 Middleware Pattern: Concept Overview
Middlewares are functions that receive a request
, response
, and a next
function. Calling next()
moves control to the next middleware or controller in the chain.
If a middleware ends the response (e.g., res.end()
), the chain is halted. This pattern is common in frameworks like ExpressJS and enables modular, reusable behavior.
🛠️ Adding Middleware Support
Let’s implement middleware chaining into the existing server.
src/app.js
Add these functions inside the App
function:
// Dispatches the handler chain
const dispatchChain = (req, res, handlers) => {
return invokeMiddlewares(req, res, handlers);
};
// Recursive function to process each middleware/controller
const invokeMiddlewares = async (req, res, handlers) => {
if (!handlers.length) return;
const current = handlers[0];
return current(req, res, async () => {
await invokeMiddlewares(req, res, handlers.slice(1));
});
};
Now modify your serverHandler
to use this chain:
const serverHandler = async (req, res) => {
const sanitizedUrl = sanitizeUrl(req.url, req.method);
const match = matchUrl(sanitizedUrl);
if (match) {
const handlers = routes.get(match);
await dispatchChain(req, res, [...handlers]);
} else {
res.statusCode = 404;
res.end('Not found');
}
};
That's all you need to support middleware chaining! 🎉
🧪 Testing Your Middleware Logic
Update your index.js
file to define and test some middleware functions.
const App = require('./src/app');
const app = App();
// Define test middlewares and controller
const mid1 = (req, res, next) => {
console.log('Middleware 1');
next();
};
const mid2 = (req, res, next) => {
console.log('Middleware 2');
next();
};
const controller = (req, res) => {
console.log('Final controller');
res.end('Hello from controller!');
};
// Add middleware + controller chain to a route
app.get('/middleware', mid1, mid2, controller);
// Add more routes if needed
app.get('/test', (req, res) => res.end('Test route'));
const start = async () => {
app.run(3000);
};
start();
Run it:
node index.js
Visit:
GET http://localhost:3000/middleware
Output:
Middleware 1
Middleware 2
Final controller
📚 Summary
- Middleware chaining allows you to break logic into reusable steps
- You implemented the Express-style
next()
pattern - Each route now supports any number of middlewares plus a final controller
✅ This prepares the framework for adding advanced features like logging, validation, authentication, etc.
▶️ Coming Up Next
In Part 3, we’ll add support for route parameters and dynamic paths — another key feature of full-featured web frameworks.
Stay tuned and happy coding! 🚀