Express is an extremely commonly used web server application framework in Node.js. Essentially, a framework is a code structure that adheres to specific rules and has two key characteristics:
- It encapsulates APIs, enabling developers to concentrate more on writing business code.
- It has established processes and standard specifications.
The core features of the Express framework are as follows:
- It can configure middleware to respond to various HTTP requests.
- It defines a route table for executing different types of HTTP request actions.
- It supports passing parameters to templates to achieve dynamic rendering of HTML pages.
This article will analyze how Express implements middleware registration, the next mechanism, and route handling by implementing a simple LikeExpress class.
Express Analysis
Let's first explore the functions it provides through two Express code examples:
Express Official Website Hello World Example
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
Analysis of the Entry File app.js
The following is the code of the entry file app.js
of the Express project generated by the express-generator
scaffolding:
// Handle errors caused by unmatched routes
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
// `app` is an Express instance
const app = express();
// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// Parse JSON format data in post requests and add the `body` field to the `req` object
app.use(express.json());
// Parse the urlencoded format data in post requests and add the `body` field to the `req` object
app.use(express.urlencoded({ extended: false }));
// Static file handling
app.use(express.static(path.join(__dirname, 'public')));
// Register top-level routes
app.use('/', indexRouter);
app.use('/users', usersRouter);
// Catch 404 errors and forward them to the error handler
app.use((req, res, next) => {
next(createError(404));
});
// Error handling
app.use((err, req, res, next) => {
// Set local variables to display error messages in the development environment
res.locals.message = err.message;
// Decide whether to display the full error according to the environment variable. Display in development, hide in production.
res.locals.error = req.app.get('env') === 'development'? err : {};
// Render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
From the above two code segments, we can see that the Express instance app
mainly has three core methods:
-
app.use([path,] callback [, callback...])
: Used to register middleware. When the request path matches the set rules, the corresponding middleware function will be executed.-
path
: Specifies the path for invoking the middleware function. -
callback
: The callback function can take various forms. It can be a single middleware function, a series of middleware functions separated by commas, an array of middleware functions, or a combination of all the above.
-
-
app.get()
andapp.post()
: These methods are similar touse()
, also for registering middleware. However, they are bound to HTTP request methods. Only when the corresponding HTTP request method is used will the registration of the relevant middleware be triggered. -
app.listen()
: Responsible for creating an httpServer and passing the parameters required byserver.listen()
.
Code Implementation
Based on the analysis of the functions of the Express code, we know that the implementation of Express focuses on three points:
- The registration process of middleware functions.
- The core next mechanism in the middleware functions.
- Route handling, with a focus on path matching.
Based on these points, we will implement a simple LikeExpress class below.
1. Basic Structure of the Class
First, clarify the main methods that this class needs to implement:
-
use()
: Implements general middleware registration. -
get()
andpost()
: Implement middleware registration related to HTTP requests. -
listen()
: Essentially, it is thelisten()
function of httpServer. In thelisten()
function of this class, an httpServer is created, parameters are passed through, requests are listened to, and the callback function(req, res) => {}
is executed.
Review the usage of the native Node httpServer:
const http = require("http");
const server = http.createServer((req, res) => {
res.end("hello");
});
server.listen(3003, "127.0.0.1", () => {
console.log("node service started successfully");
});
Accordingly, the basic structure of the LikeExpress class is as follows:
const http = require('http');
class LikeExpress {
constructor() {}
use() {}
get() {}
post() {}
// httpServer callback function
callback() {
return (req, res) => {
res.json = function (data) {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
};
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = () => {
return new LikeExpress();
};
2. Middleware Registration
From app.use([path,] callback [, callback...])
, we can see that middleware can be an array of functions or a single function. To simplify the implementation, we uniformly process the middleware as an array of functions. In the LikeExpress class, the three methods use()
, get()
, and post()
can all implement middleware registration. Only the triggered middleware varies due to different request methods. So we consider:
- Abstracting a general middleware registration function.
- Creating arrays of middleware functions for these three methods to store the middleware corresponding to different requests. Since
use()
is a general middleware registration method for all requests, the array storinguse()
middleware is the union of the arrays forget()
andpost()
.
Middleware Queue Array
The middleware array needs to be placed in a public area for easy access by methods in the class. So, we put the middleware array in the constructor()
constructor function.
constructor() {
// List of stored middleware
this.routes = {
all: [], // General middleware
get: [], // Middleware for get requests
post: [], // Middleware for post requests
};
}
Middleware Registration Function
Middleware registration means storing the middleware in the corresponding middleware array. The middleware registration function needs to parse the incoming parameters. The first parameter may be a route or middleware, so it is necessary to first determine whether it is a route. If it is, output it as it is; otherwise, the default is the root route, and then convert the remaining middleware parameters into an array.
register(path) {
const info = {};
// If the first parameter is a route
if (typeof path === "string") {
info.path = path;
// Convert to an array starting from the second parameter and store it in the middleware array
info.stack = Array.prototype.slice.call(arguments, 1);
} else {
// If the first parameter is not a route, the default is the root route, and all routes will execute
info.path = '/';
info.stack = Array.prototype.slice.call(arguments, 0);
}
return info;
}
Implementation of use()
, get()
, and post()
With the general middleware registration function register()
, it is easy to implement use()
, get()
, and post()
, just store the middleware in the corresponding arrays.
use() {
const info = this.register.apply(this, arguments);
this.routes.all.push(info);
}
get() {
const info = this.register.apply(this, arguments);
this.routes.get.push(info);
}
post() {
const info = this.register.apply(this, arguments);
this.routes.post.push(info);
}
3. Route Matching Processing
When the first parameter of the registration function is a route, the corresponding middleware function will be triggered only when the request path matches the route or is its sub-route. So, we need a route matching function to extract the middleware array of the matching route according to the request method and request path for the subsequent callback()
function to execute:
match(method, url) {
let stack = [];
// Ignore the browser's built-in icon request
if (url === "/favicon") {
return stack;
}
// Get routes
let curRoutes = [];
curRoutes = curRoutes.concat(this.routes.all);
curRoutes = curRoutes.concat(this.routes[method]);
curRoutes.forEach((route) => {
if (url.indexOf(route.path) === 0) {
stack = stack.concat(route.stack);
}
});
return stack;
}
Then, in the callback function callback()
of the httpServer, extract the middleware that needs to be executed:
callback() {
return (req, res) => {
res.json = function (data) {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
const url = req.url;
const method = req.method.toLowerCase();
const resultList = this.match(method, url);
this.handle(req, res, resultList);
};
}
4. Implementation of the next Mechanism
The parameters of the Express middleware function are req
, res
, and next
, where next
is a function. Only by calling it can the middleware functions be executed in sequence, similar to next()
in ES6 Generator. In our implementation, we need to write a next()
function with the following requirements:
- Extract one middleware from the middleware queue array in order each time.
- Pass the
next()
function into the extracted middleware. Because the middleware array is public, each timenext()
is executed, the first middleware function in the array will be taken out and executed, thus achieving the effect of sequential execution of middleware.
// Core next mechanism
handle(req, res, stack) {
const next = () => {
const middleware = stack.shift();
if (middleware) {
middleware(req, res, next);
}
};
next();
}
Express Code
const http = require('http');
const slice = Array.prototype.slice;
class LikeExpress {
constructor() {
// List of stored middleware
this.routes = {
all: [],
get: [],
post: [],
};
}
register(path) {
const info = {};
// If the first parameter is a route
if (typeof path === "string") {
info.path = path;
// Convert to an array starting from the second parameter and store it in the stack
info.stack = slice.call(arguments, 1);
} else {
// If the first parameter is not a route, the default is the root route, and all routes will execute
info.path = '/';
info.stack = slice.call(arguments, 0);
}
return info;
}
use() {
const info = this.register.apply(this, arguments);
this.routes.all.push(info);
}
get() {
const info = this.register.apply(this, arguments);
this.routes.get.push(info);
}
post() {
const info = this.register.apply(this, arguments);
this.routes.post.push(info);
}
match(method, url) {
let stack = [];
// Browser's built-in icon request
if (url === "/favicon") {
return stack;
}
// Get routes
let curRoutes = [];
curRoutes = curRoutes.concat(this.routes.all);
curRoutes = curRoutes.concat(this.routes[method]);
curRoutes.forEach((route) => {
if (url.indexOf(route.path) === 0) {
stack = stack.concat(route.stack);
}
});
return stack;
}
// Core next mechanism
handle(req, res, stack) {
const next = () => {
const middleware = stack.shift();
if (middleware) {
middleware(req, res, next);
}
};
next();
}
callback() {
return (req, res) => {
res.json = function (data) {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(data));
};
const url = req.url;
const method = req.method.toLowerCase();
const resultList = this.match(method, url);
this.handle(req, res, resultList);
};
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
}
module.exports = () => {
return new LikeExpress();
};
Leapcell: The Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis
Finally, let me introduce a platform that is very suitable for deploying Express: Leapcell.
Leapcell is a serverless platform with the following characteristics:
1. Multi-Language Support
- Develop with JavaScript, Python, Go, or Rust.
2. Deploy unlimited projects for free
- Pay only for usage — no requests, no charges.
3. Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
4. Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
5. Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ
Top comments (1)
I made this repo with each topic as a commit.
github.com/iam-arshad/expressjs-tu...
Some comments have been hidden by the post's author - find out more