If you were engaged in development for the node.js platform, then you, for certain, heard about
express.js . This is one of the most popular lightweight frameworks used when building web applications for a node.

The author of the material, the translation of which we are publishing today, proposes to study the features of the internal structure of the express framework through the analysis of its source code and consideration of an example of its use. He believes that studying the mechanisms underlying popular open source libraries contributes to their deeper understanding, removes the veil of "mystery" and helps to create better applications based on them.
You may find it convenient to keep the express source code on hand as you read this material.
This version is used here. You can read this article quite well without opening the express code, since here, wherever it is appropriate, fragments of the code of this library are given. In those places where the code is abbreviated, comments of the form
// ...
are used
// ...
Basic example of using express
To begin, take a look at the traditional “Hello World!” In the development of new computer technologies - an example. It can be found on the framework’s official website; it will serve as a starting point in our research.
const express = require('express') const app = express() app.get('/', (req, res) => res.send('Hello World!')) app.listen(3000, () => console.log('Example app listening on port 3000!'))
This code starts a new HTTP server on port 3000 and sends a
Hello World!
Response
Hello World!
on inquiries coming along the route
GET /
. If you do not go into details, you can select four stages of what is happening, which we can analyze:
- Create a new express application.
- Creating a new route.
- Start the HTTP server on the specified port number.
- Processing incoming requests to the server.
Creating a new express application
The
var app = express()
command allows you to create a new express application. The
createApplication
function from the
lib / express.js file is a function exported by default, and we refer to it by performing a call to the
express()
function. Here are some important things you should pay attention to here:
// ... var mixin = require('merge-descriptors'); var proto = require('./application'); // ... function createApplication() { // , . // : `function(req, res, next)` var app = function(req, res, next) { app.handle(req, res, next); }; // ... // `mixin` `proto` `app` // - `get`, . mixin(app, proto, false); // ... return app; }
The
app
object returned from this function is one of the objects used in the code of our application. The
app.get
method
app.get
added using the
mixin
function of the
merge-descriptors library, which is responsible for assigning
app
methods declared to
proto
. The
proto
object itself is imported from
lib / application.js .
Creating a new route
Now let's take a look at the
code that is responsible for creating the
app.get
method from our example.
var slice = Array.prototype.slice; // ... /** * `.VERB(...)` `router.VERB(...)`. */ // `methods` HTTP, ( ['get','post',...]) methods.forEach(function(method){ // app.get app[method] = function(path){ // // var route = this._router.route(path); // route[method].apply(route, slice.call(arguments, 1)); // `app`, return this; }; });
It is interesting to note that, in addition to semantic features, all methods that implement HTTP actions, such as
app.get
,
app.post
,
app.put
and the like, in terms of functionality, can be considered the same. If you simplify the above code, reducing it to the implementation of only one
get
method, you get something like this:
app.get = function(path, handler){ // ... var route = this._router.route(path); route.get(handler) return this }
Although the above function has 2 arguments, it is similar to the
app[method] = function(path){...}
. The second argument,
handler
, is obtained by calling
slice.call(arguments, 1)
.
In a nutshell,
app.<method>
simply stores the route in the application’s router using its
route
method, and then passes the
handler
to
route.<method>
.
The
route()
router method is declared in
lib / router / index.js :
// proto - `_router` proto.route = function route(path) { var route = new Route(path); var layer = new Layer(path, { sensitive: this.caseSensitive, strict: this.strict, end: true }, route.dispatch.bind(route)); layer.route = route; this.stack.push(layer); return route; };
Not surprisingly, the declaration of the
route.get
method in
lib / router / route.js looks like an
app.get
:
methods.forEach(function (method) { Route.prototype[method] = function () { // `flatten` , [1,[2,3]], var handles = flatten(slice.call(arguments)); for (var i = 0; i < handles.length; i++) { var handle = handles[i]; // ... // , , Layer, // var layer = Layer('/', {}, handle); // ... this.stack.push(layer); } return this; }; });
Each route can have several handlers; on the basis of each handler, a variable of type
Layer
constructed, which is a data processing layer, which then goes on the stack.
Layer Objects
Both
_router
and
route
use objects of type
Layer
. In order to understand the essence of such an object, let's look at its
constructor :
function Layer(path, options, fn) { // ... this.handle = fn; this.regexp = pathRegexp(path, this.keys = [], opts); // ... }
When creating objects of type
Layer
they are given a path, certain parameters, and a function. In the case of our router, this function is
route.dispatch
(we'll talk about it in more detail below, in general terms, it is intended to send a request to a separate route). In the case of the route itself, this function is a handler function declared in the code of our example.
Each object of type
Layer
has a method
handle_request , which is responsible for executing the function passed during object initialization.
Recall what happens when you create a route using the
app.get
method:
- In the application router (
this._router
) a route is created. - The
dispatch
route method is assigned as the handler method of the corresponding Layer
object, and this object is pushed onto the router's stack. - The request handler is passed to the
Layer
object as a handler method, and this object is pushed onto the route stack.
As a result, all handlers are stored inside the
app
instance as
Layer
objects that are inside the route stack, whose
dispatch
methods are assigned to
Layer
objects that are on the router's stack:
Objects of type Layer in the stack of the router and in the stack of routesIncoming HTTP requests are processed according to this logic. We will talk about them below.
Start HTTP server
After setting up the routes, you must start the server. In our example, we are
app.listen
method, passing in it the port number and the callback function as arguments. To understand the features of this method, we can refer to the
lib / application.js file:
app.listen = function listen() { var server = http.createServer(this); return server.listen.apply(server, arguments); };
Looks like
app.listen
is just a wrapper around
http.createServer
. This point of view makes sense, because if we recall what we said at the very beginning, the
app
is just a function with the signature
function(req, res, next) {...}
, which is compatible with the arguments required for
http.createServer
(the signature of this method is
function (req, res) {...}
).
After understanding that, ultimately, everything that gives us express.js can be reduced to a very intelligent handler function, the framework does not look as complicated and mysterious as it used to be.
HTTP request processing
Now that we know that the
app
is just a request handler, let us follow the path that an HTTP request passes through the express application. This path leads it to the handler declared by us.
First, the request goes to the
createApplication
function (
lib / express.js ):
var app = function(req, res, next) { app.handle(req, res, next); };
Then it goes to the
app.handle
method (
lib / application.js ):
app.handle = function handle(req, res, callback) { // `this._router` - , , `app.get` var router = this._router; // ... // `handle` router.handle(req, res, done); };
The
router.handle
method
router.handle
declared in
lib / router / index.js :
proto.handle = function handle(req, res, out) { var self = this; //... // self.stack - , // Layer ( ) var stack = self.stack; // ... next(); function next(err) { // ... // var path = getPathname(req); // ... var layer; var match; var route; while (match !== true && idx < stack.length) { layer = stack[idx++]; match = matchLayer(layer, path); route = layer.route; // ... if (match !== true) { continue; } // ... HTTP, } // ... // process_params , self.process_params(layer, paramcalled, req, res, function (err) { // ... if (route) { // `layer.handle_request` // `next` // , `next` , // , `next` , return layer.handle_request(req, res, next); } // ... }); } };
If we describe what is happening in a nutshell, the
router.handle
function goes through all the layers in the stack until it finds one that matches the path specified in the request. Then the
handle_request
method of the layer will be called, which will execute the predefined handler function. This handler function is the
dispatch
route method, which is declared in
lib / route / route.js :
Route.prototype.dispatch = function dispatch(req, res, done) { var stack = this.stack; // ... next(); function next(err) { // ... var layer = stack[idx++]; // ... layer.handle_request(req, res, next); // ... } };
In the same way as in the case of a router, during the processing of each route, the layers are
handle_request
that the route has, and their
handle_request
methods are
handle_request
, which are performed by the layer handler methods. In our case, this is the request handler, which is declared in the application code.
Here, finally, the HTTP request falls into the code area of our application.
The request path in the express applicationResults
Here we covered only the basic mechanisms of the express.js library, those that are responsible for the operation of the web server, but this library has many other features. We didn’t stop at checks that pass requests before they are received by handlers, we didn’t talk about the helper methods that are available when working with the variables
res
and
req
. And finally, we did not touch on one of the most powerful express features. It consists in the use of middleware that can be aimed at solving virtually any task - from parsing requests to implementing a full-fledged authentication system.
We hope this material has helped you understand the main features of the express device, and now you, if necessary, can understand everything else by independently analyzing the parts of the source code of this library that you are interested in.
Dear readers! Do you use express.js?
