Express.js - Architecture of Express.js
The Layered Architecture of Express.js
Express.js is a standalone web application framework for Node.js that provides its users with a robust and complete set of features for web and mobile applications along with HTTP utility methods and middleware for API development.
As stated by the team itself, Express’s mission is to guarantee a pleasant web development experience and to do that by means of an abstract layer of fundamental web application features that are accessible and easy to use.
Nonetheless, the library itself offers multiple levels of abstraction and customisation, including lower-level layers to provide access to the core qualities and features that Node.js offers.
Although the library itself is rather minimal, its structure can be regarded as a layered architecture composed of at least two main building blocks: the application layer and the backend layer.
The application layer is the layer that provides an easy-to-use tool for web and API development and that exposes methods to access all of Express’s main functionalities through its core app
object.
On the other hand, the backend layer is the backbone of the project and provides more low-level qualities and a seamless connection to Expresses’s dependencies for (amongst others) HTTP request parsing, cookie-sessions management, and a list of HTTP functionalities. Although less easy to interact with, the backend layer of express provides a more flexible way to access Node.js features.
Main Execution Environments
Express.js is a Node.js library and thus relies on a Node.js execution environment; in other words, it can be regarded as a cross-platform JavaScript runtime environment for both server and desktop applications.
The Node runtime environment was developed in 2009 for the purpose of executing JavaScript code without a browser and to enable programmers to develop full-stack applications using JavaScript. The Node runtime environment provides a variety of features unavailable, for example, in a browser environment; such as access to the server’s file system, databases and network.
It as a framework for Node.js, the natural way to deploy and use Express.js is by exploiting its object-oriented API to develop back-end applications to handle HTTP requests and responses using its interface for routing and middleware handling.
Components view
The main components of the Express library are the Application, Request, Response, and Router. Together, these components make up a lightweight backend framework that provides routing and middleware functionality.
The Request and Response components are simply HTTP requests and responses. A Request is an HTTP request that our app receives at a specific endpoint. Usually, the endpoint, method, and parameters of the request are used to determine what kind of a response to send to the client.
The application is the component that handles all of the routing and middleware of the web application. It has a built-in router component, more of which can be created. A router is in a sense a mini-application. It has the same routing and middleware functionality as the top application component, but it is not an application on its own as it does not listen to new requests. It can be added to the application to create modular routing for specific endpoints in the application.
Connectors view
The Application component is the center component of the Express library, as it makes use of Request, Response and Router components to create a functioning web application. Upon receiving an HTTP request, the app calls its internal router, which then calls a stack of middleware defined for that route, sets the response, and renders it.
The request and response are parameters for middleware called by the router, and the router (or set of routers) are variables used by the application component.
Development view
The Express library is composed of a single library with a module for handling routing, and a module that exposes additional middleware.
All of Express’s functionality is exported through a single function (createApplication()
in express.js
), which returns the main app object. Additionally, routing functionality (via the Route
and Router
objects in router/
) and all middleware are exposed to the user via properties that are attached to the createApplication()
object.
Here we can see how the project is structured and which modules from the Node standard library are used
Looking at the graph, one might question where all HTTP functionality is and that the project is quite small, which is true. Most of Express’s low-level functionality (such as URL and request parsing) are included in packages from the jshttp
and pillarjs
project.
In this graph1 we can see which files from the Express project depend on which external packages
Run time view
At its core, all Express does is take in a HTTP request, run it through some callbacks and return a HTTP response. Express is in essence a very nice wrapper around Node’s http
and net
modules with additional functionality.
To get an idea of how the system works at runtime, we’ll look at how the application is initialized and go over what a typical end-to-end HTTP flow would look like, from incoming requests to the returned HTTP response.
We’ll consider a simple app which where you can add and view a list of TODOs. For the example, we will use a HTTP GET request for the /todo/1
path.
The runtime of Express can be summarized in the following diagram.
In the next sections, we’ll dive deeper into each part of the diagram.
Init sequence
The default export for Express is the createApplication()
function, when creating an application this function would be called, which returns a application.app
object.
After creating the app object, you would typically setup the middlewares2 that the app uses and then start the app using the listen()
function.
The listen()
function starts an http server. This creates a http.Server
object (from the Node standard library) in the background.
HTTP flow
After the app has been successfully started, it will begin listening for HTTP requests. When the HTTP server receives a request, it will be forwarded to the app, which then dispatches the request using Express’s router.Router
(if it has been set up).
The router proceeds to pass the HTTP request through a stack of middlewares (which in Express’s case, is just a Javascript function).
At a high level can we can distinguish between 2 types of middlewares, those for processing a request (such as checking for authentication), and those that route requests.
Each “layer” (router/layer.js
) of the stack has criteria for whether it should be applied to the request, which it checks with its match()
function.
After passing through the middleware stack, the response is passed to the finalhandler()
function, which finishes the request by e.g. setting the status code, creating an error message if an error occurred, etc. The created HTTP response is then returned to the user by http.Server
.
Qualities vs Architecture
Express’ key qualities identified in our previous post find support in the design and coding practices used in the framework. Specifically, qualities and the implementation of Express are related as follows:
-
Simplicity – to ensure simplicity in its design, Express’ code structure is streamlined as much as possible. When opening the framework’s codebase, one is presented with just 4 folders: 3 of these contain tests, examples of features implementations, and benchmarks; one folder packages together with the library of Express, contained in only 11 JavaScript files (6 for core functionalities, 2 for middleware and 3 for the router).
-
Modularity, configurability and extensibility – Express thrives on providing its users and developers with an environment that can be used as a building base for own APIs and middleware. For such, the example applications proposed in the framework’s repository clearly address the modular nature of Express’ components: every functionality there presented can be implemented in a standalone fashion as easily as if it were to be included in a more complex application context. Moreover, due to its unopinionated and lightweight Node-framework’s nature, Express is completely configurable in its parameters and offers the full capability of overriding its own API. Finally, these features provide the optimal scenario for developers to code and distribute their own middleware and extensions to Express, as an entire page of their website is dedicated to this.
-
Performance and scalability – Express cares about their user satisfaction for what concerns its performance. For such, a page on their website only focuses on how to improve the performance of an Express application, including direct optimizations that can be made both in code and in release options. Code-wise, it is important that the asynchronous nature of Express is exploited at its fullest potential, as any call to synchronous functions ties up the server until it returns. Using the native compression middleware for sending the response body is highly suggested, as well as ensuring to make use of the production Node Environment - a special environment in which the Express application runs that ensures caching of view templates, caching of CSS files generated from CSS extensions and less verbose error messages generation. Finally, Express highly relies on scalability over a cluster of processes to improve performance and does so via Node’s cluster module.
These qualities, however, need to trade off with their opposite counterparts. Simplicity, for instance, tends to clash with accuracy: this is mitigated by providing a predefined set of options for most features that ensure out-of-the-box functioning, although they can be fine-tuned and specified by developers whenever more granular control over them is required.
On the other hand, performance and scalability need to deal with usability and interoperability: in order to maintain an unopinionated approach to Node and grant direct access to Node functionalities whenever needed by the user, Express does not provide as deep of an optimization at the HTTP-handling stage as other similar frameworks do (e.g., fastify or koa); in order to maintain performance and scalability, Express explicitly states that “crashing and restarting is often the most reliable way to recover from an error” thanks to its fast start-up times, directly impacting the interoperability of the system and the possibility to always have access over it.
API Design Principles in Express
Whenever a developer approaches an Express application for the first time, they find immediately clear what each of its components does: an application context is called via the function express()
, then all of its routing and requests handling is managed by unambiguous middleware (for instance, the get()
function that deals with GET
requests); requests themselves – as well as responses – feature their own set of characteristic functions that offer the features that one would expect them to implement. This is the result of applying the Principle of Least Surprise during the design phase of Express, meaning that the user is provided with a code structure that reflects how each component of the system should intuitively behave.
As Express is explicitly developer-oriented towards designing new APIs and frameworks with it, it advocates and follows the Open-Closed Principle, which states that a change in behaviour should be achieved without the need to change the existing – Express’ own – code; instead, the change in behavior should happen via extension points and by coding on top of the existing functionalities, in order to generate new system components.
-
Generated using
dependency-cruiser
on Express v4.17.3 ↩︎ -
A design pattern that is commonly used to stack additional functionality or provide additional services. ↩︎