This article gives an overview of Express and how it can be used to build complex HTTP server applications based on the Node.js http
module.
Let's create a simple Express server with Node:
1 2 3 4 5 6 7
const http = require("http"); const express = require("express"); const app = express(); http.createServer(app).listen(3000, () => { console.log("Express server listening on port 3000"); });
So, what is app
here? The http.createServer()
function is part of the Node.js http
module. Inspection of the Node.js documentation shows that createServer()
expects a request listener as its argument. This is a function that takes two arguments: a request object and a response object, named here respectively: req
and res
.
1 2 3
function requestListener(req, res) { // ... }
Therefore, app
must be (and actually is) a request listener function:
1 2 3
function app(req, res) { // ... }
But actually, app
is far more than that. The Express package adds number of properties to the app
function. Remember that a JavaScript function is in essence a JavaScript object, although one of a special type. You can add properties to any JavaScript object, including to function objects, such as app
here.
Below is a simplified representation of the internals of Express:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// Simplified internal implementation of the 'express' Node module function createApplication() { const app = function(req, res) { // ... }; app.use = function use(...middleWareFns) { // adds middleware shared across all routes }; app.get = function get(path, ...middleWareFns) { // adds middleware for the GET method for a specific route }; // etc. return app; } module.exports = createApplication;
Figure 1 below shows a real world (though trimmed down) example of an Express application (this one is taken from the Hyfer application). We will dissect this code snippet in the next sections.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
const http = require("http"); const express = require("express"); const compression = require("compression"); const bodyParser = require("body-parser"); const cookieParser = require("cookie-parser"); const serveStatic = require("serve-static"); const app = express(); // shared middleware for all routes app.use(bodyParser.json()); app.use(cookieParser()); app.use(serveStatic("./public")); // route specific request handlers app.get("/api/timeline", getTimeLine); app.get("/api/user/:id", isAuthenticated, getUser); http.createServer(app).listen(3000, () => { console.log("Express server listening on port 3000"); }); // example request handler implementations function getTimeLine(req, res) { //... res.json(timeline); } function getUser(req, res) { //... res.json(user); } function isAuthenticated(req, res, next) { //... if (authenticated) { next(); } else { // unauthorized res.sendStatus(401); } }
Listing 1: A real world Express application
In the Express documentation the term middleware is used to describe a special type of request handler. It has the following function signature:
1 2 3
function middlewareFn(req, res, next) { // ... }
Note that a third parameter, next
is added to the function signature. This next
parameter is a function that takes no parameters.
Express constructs a pipeline of middleware functions through which an incoming HTTP request (req
) is routed and an outgoing HTTP response (res
) is sent back.
Each middleware function is expected to either pass on the request to the next function in the pipeline, i.e. by calling next()
, or send a response itself. In the latter case, handling of the request ends there and then. Otherwise the next middleware function in the pipeline is called.
Middleware functions can (and usually do) make change to the request object, the response object or both, before passing them on. In fact, this is the main purpose of middleware: adding functionality and information as the request progresses through the pipeline.
Ultimately there must be some middleware function in the pipeline that sends a response. If not, the client will time out with an error.
Figure 1 below illustrates the middleware configuration of Listing 1 above.
Figure 1. Express middleware pipeline representation of Listing 1 code.
All incoming requests are routed through the common middleware functions added and configured through app.use()
. Next, the requests are routed to middleware functions based on the request method (e.g. a GET
) and request path (e.g. /api/timeline
). These functions are usually added and configured through app.get()
, app.post()
, etc.
Application specific routes are ultimately handled by request handlers at leaf nodes of the pipeline. Because they are leaf nodes, they usually leave out the next
parameter, as there is no 'next' to pass the request on to.
In Listing 1 and Figure 1 there are two middleware functions that conditionally call next()
.
The first one comes from the serve-static middleware. This middleware checks whether the requested URL corresponds to a file (e.g. index.html
) in the server folder designated for hosting static content. If so, this file is served as the response. If not, the middleware function calls next()
to continue the pipeline.
The second one in this example is the function isAuthenticated()
which test whether the requesting user is authenticated (details on how this is done is left out here). If the requesting user is an authenticated user, then, the request is allowed to pass through. Otherwise an HTTP 401 - Unauthorized
is sent back, without allowing the request to continue.
There is far more to Express than can be covered in this article.