DEV Community

Cover image for Mastering Express.js: A Deep Dive
Leapcell
Leapcell

Posted on

Mastering Express.js: A Deep Dive

Image description

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

From the above two code segments, we can see that the Express instance app mainly has three core methods:

  1. 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.
  2. app.get() and app.post(): These methods are similar to use(), 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.
  3. app.listen(): Responsible for creating an httpServer and passing the parameters required by server.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() and post(): Implement middleware registration related to HTTP requests.
  • listen(): Essentially, it is the listen() function of httpServer. In the listen() 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");
});
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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 storing use() middleware is the union of the arrays for get() and post().

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
    };
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
    };
}
Enter fullscreen mode Exit fullscreen mode

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 time next() 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();
}
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

Leapcell: The Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis

Image description

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)

Collapse
 
iam-arshad profile image
Info Comment hidden by post author - thread only accessible via permalink
ARSHAD BASHA

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