Express is perhaps the most popular Node.js web app framework. It's so popular that it's part of the MEAN Stack acronym. MEAN refers to MongoDB, ExpressJS, AngularJS, and Node.js. Express is described as being Sinatra-like, referring to a popular Ruby application framework, and that it isn't very opinionated. This means Express is not at all strict about how your code is structured; you just write it the way you think is best.
You can visit the home page for Express at http://expressjs.com/.
Shortly, we'll implement a simple application to calculate Fibonacci numbers using Express, and in later chapters, we'll do quite a bit more with Express. We'll also explore how to mitigate the performance problems from computationally intensive code we discussed earlier.
Let's start by installing the Express Generator. While we can just start writing some code, the Express Generator gives a blank starting application. We'll take that and modify it.
Install it using following commands:
$ mkdir fibonacci $ cd fibonacci $ npm install [email protected]
This is different from the suggested installation method on the Express website, which was to use the -g
tag for a global install. We're also using an explicit version number to ensure compatibility.
Earlier, we discussed how many now recommend against installing modules globally. In the Twelve-Factor model, it's strongly recommended to not install global dependencies, and that's what we're doing.
The result is that an express
command is installed in the ./node_modules/.bin
directory:
$ ls node_modules/.bin/ express
To run the command:
$ ./node_modules/.bin/express --help Usage: express [options] [dir] Options: -h, --help output usage information -V, --version output the version number -e, --ejs add ejs engine support (defaults to jade) --hbs add handlebars engine support -H, --hogan add hogan.js engine support -c, --css <engine> add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css) --git add .gitignore -f, --force force on non-empty directory
We probably don't want to type ./node_modules/.bin/express
every time we run the Express Generator application, or, for that matter, any of the other applications that provide command-line utilities. Fortunately, modifying the PATH
environment variable is an easy solution:
$ export PATH=node_modules/.bin:${PATH}
This works for a bash
shell. But, for csh
users, try this:
$ setenv PATH node_modules/.bin:${PATH}
With the PATH
environment variable set like this, the express
command can be directly executed. Let's now use it to generate a blank starter application.
Now that you've installed express-generator
in the fibonacci
directory, use it to set up the blank framework application:
This created for us a bunch of files which we'll walk through in a minute. The node_modules
directory still has the express-generator
module, which is now not useful. We can just leave it there and ignore it, or we can add it to the devDependencies
of the package.json
it generated. Alternatively, we can uninstall it as shown in the preceding screenshot.
The next thing to do is run the blank application in the way we're told. The command shown, npm start
, relies on a section of the supplied package.json
file:
"scripts": { "start": "node ./bin/www" },
The npm tool supports scripts that are ways to automate various tasks. We'll use this capability throughout the book to do various things. Most npm scripts are run with the npm run scriptName
command, but the start
command is explicitly recognized by npm and can be run as shown previously.
The first step is to install the dependencies (npm install
), then to start the application:
We can modify the supplied npm start
script to print debugging information on the screen. Change the scripts
section to the following:
"scripts": { "start": "DEBUG=fibonacci:* node ./bin/www" },
Adding DEBUG=fibonacci:*
this way sets an environment variable, DEBUG
, as shown in the screenshot. In Express applications, the DEBUG
variable turns on debugging code, which prints an execution trace that's useful for debugging.
Since the output says it is listening on port 3000
, we direct our browser to http://localhost:3000/
and see the following output:
We have a working blank Express application; let's look at what was generated for us. We're doing this to familiarize ourselves with Express before diving in to start coding our Fibonacci application.
Because we used the --ejs
option, this application is set up to use the EJS template engine. The template engine chosen assists with generating HTML. EJS is documented at http://ejs.co/.
The views
directory contains two files, error.ejs
and index.ejs
. These are both EJS templates that we'll modify later.
The routes
directory contains the initial routing setup, that is, the code to handle specific URLs. We'll modify these later.
The public
directory will contain assets that the application doesn't generate, but are simply sent to the browser. What's initially installed is a CSS file, public/stylesheets/style.css
.
The package.json
file contains our dependencies and other metadata.
The bin
directory contains the www
script that we saw earlier. That's a Node.js script, which initializes the HTTPServer
objects, starts it listening on a TCP port, and calls the last file we'll discuss, app.js
. These scripts initialize Express, hook up the routing modules, and do other things.
There's a lot going on in the www
and app.js
scripts, so let's start with the application initialization. Let's first take a look at a couple of lines in app.js
:
var express = require('express'); … var app = express(); … module.exports = app;
This means that app.js
is a module that exports the object returned by the express
module.
Now, let's turn to the www
script. The first thing to see is it starts with this line:
#!/usr/bin/env node
This is a Unix/Linux technique to make a command script. It says to run the following as a script using the node
command.
It calls the app.js
module as follows:
var app = require('../app'); … var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); … var server = http.createServer(app); … server.listen(port); server.on('error', onError); server.on('listening', onListening);
We see where port 3000
comes from; it's a parameter to the normalizePort
function. We also see that setting the PORT
environment variable will override the default port 3000
. Try running the following command:
$ PORT=4242 DEBUG=fibonacci:* npm start
The application now tells you that it's listening on port 4242
, where you can ponder the meaning of life.
The app
object is next passed to http.createServer()
. A look in the Node.js documentation tells us this function takes a requestListener
, which is simply a function that takes the request
and response
objects we've seen previously. This tells us the app
object is such a function.
Finally, the www
script starts the server listening on the port we specified.
Let's now walk through app.js
in more detail:
app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs');
This tells Express to look for templates in the views
directory and to use the EJS templating engine.
The app.set
function is used for setting application properties. It'll be useful to browse the API documentation as we go through (http://expressjs.com/en/4x/api.html).
Next is a series of app.use
calls:
app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', routes); app.use('/users', users);
The app.use
function mounts middleware functions. This is an important piece of Express jargon we will discuss shortly. At the moment, let's say that middleware functions are executed during the processing of routes. This means all the features named here are enabled in app.js
:
body-parser
module handles parsing HTTP request bodies. Visit https://www.npmjs.com/package/body-parser for its documentation.cookie-parser
module is used to parse HTTP cookies. Visit https://www.npmjs.com/package/cookie-parser for its documentation.public
directory.routes
and users
, to set up which functions handle which URLs.Let's round out the walkthrough of app.js
by discussing what middleware functions do for our application. We have an example at the end of the script:
app.use(function(req, res, next) { var err = new Error('Not found'); err.status = 404; next(err); });
The comment says catch 404 and forward to error handler
. As you probably know, an HTTP 404
status means the requested resource was not found. We need to tell the user their request wasn't satisfied, and this is the first step in doing so. Before getting to the last step of reporting this error, you must learn how middleware works.
We do have a middleware function right in front of us. Refer to its documentation at http://expressjs.com/en/guide/writing-middleware.html.
Middleware functions take three arguments. The first two, request
and response
, are equivalent to the request
and response
of the Node.js HTTP request object. However, Express expands the objects with additional data and capabilities. The last, next
, is a callback function controlling when the request-response cycle ends, and it can be used to send errors down the middleware pipeline.
The incoming request gets handled by the first middleware function, then the next, then the next, and so on. Each time the request is to be passed down the chain of middleware functions, the next
function is called. If next
is called with an error object, as shown here, an error is being signaled. Otherwise, the control simply passes to the next middleware function in the chain.
What happens if next
is not called? The HTTP request will hang because no response has been given. A middleware function gives a response when it calls functions on the response
object, such as res.send
or res.render
.
For example, consider the inclusion of app.js
:
app.get('/', function(req, res) { res.send('Hello World!'); });
This does not call next
, but instead calls res.send
. This is the correct method of ending the request-response cycle, by sending a response (res.send
) to the request. If neither next
nor res.send
is called, the request never gets a response.
Hence, a middleware function does one of the following four things:
body-parser
and cookie-parser
do so, looking for data to add to the request
object.next
to proceed to the next middleware function or else signals an error.The ordering of middleware execution depends on the order they're added to the app
object. The first added is executed first, and so on.
We've seen two kinds of middleware functions so far. In one, the first argument is the handler function. In the other, the first argument is a string containing a URL snippet, and the second argument is the handler function.
What's actually going on is app.use
has an optional first argument the path the middleware is mounted on. The path is a pattern match against the request URL, and the given function is triggered if the URL matches the pattern. There's even a method to supply named parameters in the URL:
app.use('/user/profile/:id', function(req, res, next) { userProfiles.lookup(req.params.id, (err, profile) => { if (err) return next(err); // do something with the profile // Such as display it to the user res.send(profile.display()); }); });
This path specification has a pattern, :id
, and the value will land in req.params.id
. In this example, we're suggesting a user profiles service, and that for this URL we want to display information about the named user.
Another way to use a middleware function is on a specific HTTP request method. With app.use
, any request will be matched, but in truth, GET
requests are supposed to behave differently than POST
requests. You call app.METHOD
where METHOD
matches one of the HTTP request verbs. That is, app.get
matches the GET
method, app.post
matches POST
, and so on.
Finally, we get to the router
object. This is a kind of middleware used explicitly for routing requests based on their URL. Take a look at routes/users.js
:
var express = require('express'); var router = express.Router(); router.get('/', function(req, res, next) { res.send('respond with a resource'); }); module.exports = router;
We have a module whose exports
object is a router. This router has only one route, but it can have any number of routes you think is appropriate.
Back in app.js
, this is added as follows:
app.use('/users', users);
All the functions we discussed for the app
object apply to the router
object. If the request matches, the router is given the request for its own chain of processing functions. An important detail is that the request URL prefix is stripped when the request is passed to the router instance.
You'll notice that the router.get
in users.js
matches '/'
and that this router is mounted on '/users'
. In effect that router.get
matches /users
as well, but because the prefix was stripped, it specifies '/'
instead. This means a router can be mounted on different path prefixes, without having to change the router implementation.
Now, we can finally get back to the generated app.js
, the 404
error (page not found), and any other errors the application might want to show to the user.
A middleware function indicates an error by passing a value to the next
function call. Once Express sees an error, it will skip any remaining non-error routing, and it will only pass it to error handlers instead. An error handler function has a different signature than what we saw earlier.
In app.js
that we're examining, this is our error handler:
app.use(function(err, req, res, next) { res.status(err.status || 500); res.render('error', { message: err.message, error: {} }); });
Error handler functions take four parameters, with err
added to the familiar req
, res
, and next
. For this handler, we use res.status
to set the HTTP response status code, and we use res.render
to format an HTML response using the views/error.ejs
template. The res.render
function takes data, rendering it with a template to produce HTML.
This means any error in our application will land here, bypassing any remaining middleware functions.