Express is a framework built on top of Node's core http (and https when relevant) module.
See Chapter 5, Wielding Web Protocols for more on Node's core http module.
Express decorates the req (http.IncomingMessage) and res (http.ServerResponse) objects, which are passed to the http.createServer request handler function.
To explain this using code, at a very basic level Express essentially performs the following internally:
const http = require('http') http.createServer((req, res) => { /* add extra methods and properties to req and res */ }))
When we call the express function, it returns an instance that we called app , which represents our Express server.
The app.use function allows us to register middleware, which at a fundamental level is a function that is called from the same http.createServer request handling function.
Again, for a pseudo-code explanation:
const http = require('http') http.createServer((req, res) => { /* call the middleware registered with app.use */ /* wait for each piece of middleware to finish before calling the next (wait for the next cb) */ }))
Each piece of middleware may call methods on req and res, and extend the objects with additional methods or properties.
The express.static method comes bundled with Express. It returns a middleware function that is passed into app.use. This function will attempt to locate a file based on supplied configuration (in our case, we set the root directory to the public folder) for a given route. Then it will create a write stream from the file and stream it to the request object (req). If it can't find a file it will pass control to the next middleware by calling the next callback.
We only use the static middleware in development mode (based on the value of the dev reference, which is assigned based on whether the NODE_ENV environment variable is set to production). This assumes a production scenario where a reverse proxy (such as Nginx or Apache or, even better a CDN) handles static file serving. While Node has come a long way in recent years, Node's strength remains in generating dynamic content - it still doesn't usually make sense to use it for static assets in production.
The order of middleware is significant, because middleware executes in cascading fashion. For instance, if we register static file handling middleware before route handling middleware in the case of name collision (where a route could apply to a file or a dynamic route), the file handling middleware will take precedence. However, if the route handling middleware is first, the dynamic route will serve the request first instead.
The app.use function can accept a string as the first argument, which determines a mount point for a piece of middleware. This means instead of the middleware applying to all incoming requests it will only be called when there is a route match.
Route handlers are essentially the same mounted middleware, but are constructed with Express' Router utility for cleaner encapsulation. In our routes/index.js file we create a router object, which we called router. Router objects have methods that correspond to the HTTP verbs (such as GET, PUT, POST, PATCH, DELETE). The full HTTP specification can be found here: (https://tools.ietf.org/html/rfc7231).
Most commonly we would use GET and POST for web-facing applications. We use router.get to register a route (/), and supply a route handling function (which is technically also middleware).
In our route handler, we pass res.send a string of HTML content to respond to the client.
The res.send method is added by Express, it's the equivalent of res.end , but with additional features such as content type detection.
We export the router instance from routes/index.js, then load it into the index.js file and pass it to app.use (as the second argument, after a mount point string argument (/)).
The router instance is itself, middleware. It's a function that accepts req, res, and next arguments. When called, it checks its internal state based on any routers registered (via get and so on), and responds accordingly.
The function we pass to router.get can also take a next callback function. We ignored the next callback function in our case (we didn't define it in the route handling functions parameters), because this route handler is a terminal point - there is nothing else to be done after sending the content. However, in other scenarios there may be cause to use the next callback and even pass it an error to propagate request handling the next piece of middleware (or route middleware, since a route registering method (such as get) can be passed multiple subsequent route handling functions).
At the end of index.js we call app.listen and pass it a callback function. This will in turn call the listen method on the core http server instance that Express has created internally, and pass our supplied callback to it. Our callback simply logs that the server is now listening on the given port.
While Express can work with HTTPS, we recommend that the general approach should be to terminate SSL at the load balancer (or reverse proxy) for optimal efficiency.