- Compare and contrast the structure of an Express.js app vs one made using the
http
module. - Build a simple application using Express and set it running on a port.
- Configure basic routes and enable CORS.
- Explain and use the express concepts of "handler chains" and "middleware"
- Use the
body-parser
middleware to parse POST and PUT/PATCH requests. - Incorporate middleware
morgan
anderrorhandler
for handling logs and error messages.
Express is a (relatively) lightweight server-side web framework that's written in JavaScript. We've already seen, with the help of the http
module, how a web application can be built up in Node; with Express, we'll take it to the next level and make our apps configurable and easily extensible.
Consider a simple web application - People and Pets, where a Person has many Pets. How might we have set that up?
Maybe something like this?
.
├── app.js
├── controllers
│ ├── applicationController.js
│ ├── peopleController.js
│ └── petsController.js
├── models
│ ├── index.js
│ ├── person.js
│ └── pet.js
├── package.json
└── scripts
├── seed.js
├── testPersonModel.js
└── testPetModel.js
Here's how an equivalent Express application might look. Keep in mind that both Node and Express are super-unopinionated about how we structure our applications, so to a certain extent this structure is arbitrary; we've given you this structure for a reason, though, as you'll see soon.
.
├── app.js
├── bin
│ └── www
├── models
│ ├── index.js
│ ├── person.js
│ └── pet.js
├── package.json
├── routes
│ ├── index.js
│ ├── people.js
│ └── pets.js
└── scripts
├── seed.js
├── testPersonModel.js
└── testPetModel.js
Not too dissimilar, right? In fact, if we were to look closely at both models
folders, we might even discover that they were identical!
One interesting thing about Express that distinguishes it from Rails is that it has no built-in conception of a database - we have to explicitly link it up to whatever sort of data store we're using. In other words, pretty muchg all that Express does is routing, control, and (as appropriate) handling server-side view rendering.
Let's work through building out a very basic Express app together. Please code along.
First, let's get our project set up. Run npm init
to create a package.json
file for our new app, and then run npm install --save express
to download Express.
Next, open up a new file called app.js
and write the following inside of it.
var express = require('express'),
http = require('http');
var app = express();
That's it! We've created our empty Express app. The only thing left to do is to serve it.
At the bottom of your main JS file, write the following:
var port = 8000;
var server = http.createServer(app);
server.listen(port, function(){
console.log("Server is running on port " + port);
});
Finally, in the middle of the file, write the following:
app.get('/*', function (req, res) {
res.send('Hello World!');
});
The req object is a http.IncomingMessage object. This is what we used in the simple HTTP node server. The res object is http.ServerResponse object. Also used in the simple HTTP node server
This last thing we've just added is a route - this is what routes look like (more or less) in Express.
To start your new server, just enter node app.js
in the console and take a look at port 8000. Try visiting a variety of urls - what do you observe?
As you can see, defining routes in Express is pretty straightforward. Here's how we might take our app and define some basic routes.
app.get('/people', function(req, res){
res.send("people#index");
});
app.post('/people', function(req, res){
res.send("people#create");
})
Extracting things like IDs from urls is extremely easy with Express - much simpler than doing it by hand.
app.get('/people/:id', function(req, res){
res.send("people#show");
});
app.patch('/people/:id', function(req, res){
res.send("people#update");
});
app.delete('/people/:id', function(req, res){
res.send("people#destroy");
});
You can also use the route
method to define multiple routes as a single expression.
app.route('/people/:id')
.get(function(req, res){
res.send("people#show");
})
.patch(function(req, res){
res.send("people#update");
})
.delete(function(req, res){
res.send("people#destroy");
});
Another option is to create small, modular 'mini-routers', which can then be re-integrated back into Express. This is a common approach when you have lots of routes, and in fact is also the approach being followed in the example above - each file inside the routes
directory holds a single mini-router, set up as follows.
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) { // index
res.send("people#index");
});
router.post('/', function(req, res, next) { // create
res.send("people#create");
});
router.get('/:id', function(req, res, next) { // show
res.send("people#show");
});
router.patch('/:id', function(req, res, next) { // update
res.send("people#update");
});
router.delete('/:id', function(req, res, next) { // destroy
res.send("people#destroy");
});
module.exports = router;
These separate stand-alone routers then get brought back together in the main JS file which require
s them.
...
app.use('/', require('./routes/index'));
app.use('/people', require('./routes/people'));
app.use('/pets', require('./routes/pets'));
...
In the context of Express, these little 'plug-ins' that get added into the app are called middleware. More on middleware later in this lesson.
With every route is a handler function which determines how the app will respond to any given request. In Express, a handler will usually take three arguments, but some take two or four.
The three arguments to an ordinary Express handler are req, which is the HTTP request object that came from the user; res, which is the HTTP response object being prepared by Express; and next, which is a callback.
You can read all about the HTTP Request and HTTP Response objects in the Node or Express documentation, but you'll probably find it easier going if you just dip in when you need something specific.
You may have noticed that even though we said handlers could have three arguments, the ones we have used so far only have two. This is because they are terminal handlers: they contain a statement in them that indicates that our processing of the request is done and the server should send a response. Terminal handlers do not have a next function for that reason.
Some of the statements that end processing are here:
Response method | What it means |
---|---|
res.end() |
End the response process. |
res.json(jsObject) |
Send a JSON response. |
res.redirect() |
Redirect a request. |
res.sendStatus() |
Set the response status code and send its string representation as the response body. |
Of course, if some handlers are terminal, that means others must be non-terminal; in Express, non-terminal handlers are chainable - the program can flow from one handler to the next. The ability to chain handlers is part of what makes Express so powerful and flexible despite its bare-bones simplicity.
Take a look at the following example.
app.get('/contacts', function(req, res, next) {
if (!res.locals.contacts) {
res.locals.contacts = [];
}
// add first group of contacts (from iPhone/iCloud?)
res.locals.contacts.push({
name: 'David',
phone: '111-222-3333'
});
next();
});
app.get('/contacts', function(req, res, next) {
if (!res.locals.contacts) {
res.locals.contacts = [];
}
// add second group of contacts (from Google/Android?)
res.locals.contacts.push({
name: 'Brian',
phone: '444-555-6666'
});
next();
});
app.get('/contacts', function(req, res, next) {
if (!res.locals.contacts) {
res.locals.contacts = [];
}
// add third group of contacts (from Hotmail?)
res.locals.contacts.push({
name: 'Alex',
phone: '777-888-9999'
});
next();
});
app.get('/contacts', function(req, res) {
res.json(res.locals.contacts);
res.status(200);
});
Look at the page in your browser and notice that the handlers were invoked in the order we defined them.
res.locals
is a property of the response object that is explicitly for handler functions to store information in. It persists through the life of the request/response, and is shared across middleware and handlers.
Also notice that we have three ordinary handlers (req, res, next as arguments and one terminal handler (only req and res as arguments, and one of our statements that end processing.
What do you think happens if we do not have a terminal handler? Try it in your browser and see. Why do you think that happens?
In this case, the handler chain is simple enough that Express can see that it will never terminate. However, if you do something more complex, the server will simply never respond to that request.
Chained handlers might seem silly at first blush: in the earlier example, very little prevented us from just writing this:
app.get('/contacts', function(req, res) {
res.json([{
name: 'David',
phone: '111-222-3333'
}, {
name: 'Brian',
phone: '444-555-6666'
}, {
name: 'Alex',
phone: '777-888-9999'
}]);
res.status(200);
});
And in fact, in real apps you probably won't write three handlers to do basically the same thing with different strings.
What you will most likely do, however, is write an authentication handler, a content handler, or a security logging handler that needs to run for certain routes.
Being able to chain handlers means that you can make your code modular and run only the modules you need (when you need them!) for any given request.
Most web frameworks have this kind of HTTP Request Processing mechanism. For example, in Rails we have before_actions that are invoked for specific controller actions.
In your project groups, create a simple Express app with a resource 'movies' that responds with JSON (from hard-coded JS objects).
As you build a number of different applications in Express, you'll find that there are a few pieces of Express middleware that you'll be reaching for over and over again - they're found in almost every project.
Middleware | Package Name | Purpose |
---|---|---|
Serve-Static | N/A - now bundled in Express | Serve up static pages. |
CORS | cors |
Create a CORS policy for the app. |
BodyParser | body-parser |
Easily read the body of an incoming request. |
Morgan | morgan |
Logging. |
ErrorHandler | errorhandler |
Self-explanatory. |
Let's dive into each of these in depth.
Remember how, when we were building applications in Rails, we would have a public
folder for holding static HTML and other assets? Well, serve-static allows us to do the same thing in Express. serve-static
used to be stand-alone middleware, but ever since ___ it's officially been part of Express, which makes it especially easy to set up - just add the line below to your list of middleware
app.use(express.static('public'));
and create a directory called public
. You're done! Now any static asset (HTML, CSS, JS, images) you put inside public
will be served up automatically.
In your teams, create a new small Express app that serves up one of the following three static images (or any other three images that you'd prefer to serve):
Hope you didn't forget about your old friend CORS! Fortunately, dealing with CORS is Express is pretty easy - just download the CORS middleware via NPM, and then require and use it inside app.js
.
Console
npm install --save cors
app.js
var cors = require('cors');
...
app.use(cors());
This will give us a blanket white-list CORS policy, which isn't so great for real life, but is fine for now.
Reading the body of an incoming request is mission-critical for just about every possible web application. body-parser
gives us an easy interface for reading that request body, so that we don't have to worry about (a) loading data chunks one at a time, or (b) making sure that the body is in the right format.
Console
npm install --save body-parser
app.js
var bodyParser = require('body-parser');
Once you've require
d it, you'll need to configure body-parser
. You have a couple of different options with this, but the primary one that you'll be using is
app.js
app.use(bodyParser.json())
This will add an additional property, .body
to the request object that your middleware interacts with, which you can then immediately use to grab data.
routes/person.js
router.post('/', function(req, res) { // create
models.Person.create(req.body).then(function(person){
res.json(person);
}, function(err){
console.error(err);
});
// res.send("people#create");
});
In groups, make another small Express app from scratch. This time, set up a database with a single table using Sequelize and set up standard CRUD routes.
Hint: If you're feeling stuck, look back to the example app from the beginning of the lesson!
Back when we were working with Rails, you may or may not have noticed a directory inside your projects called log
; this is where your Rails app would keep an ongoing record of everything it's ever done. This can be an extremely useful tool for debugging, and is really a core feature for just about any web application.
Naturally, there's a tool to do this in Express as well - morgan
. To use it, download and require it in the usual way.
Before you use it, there are some configuration settings you'll probably want to use.
// create a write stream (in append mode)
var accessLogStream = fs.createWriteStream('logs/access.log', {flags: 'a'});
// setup the logger
app.use(morgan('combined', {stream: accessLogStream}));
Then, create a new directory called logs
in the root of your project. This is where your logfile will be created.
errorhandler
, as the name implies, is a piece of middleware that's designed to help manage how errors are handled. Unlike the other middleware mentioned so far, errorhandler
is for use in development only; this is because errorhandler
sends its full stack trace back to the client anytime there's an error, and that is not something you want to happen in production.
Console
npm install --save-dev errorhandler
app.js
var errorhandler = require('errorhandler');
...
...
if (process.env.NODE_ENV === 'development') {
// only use in development
app.use(errorhandler())
}
One other thing to be aware of with Express that 404 errors are not handled by default, so you'll need to create a catch-all route to handle them. An example of this is below:
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
// Error handler is `use`d next, so 'next' refers to it. This line calls that error handler with the new error
});
if (process.env.NODE_ENV === 'development') {
// only use in development
app.use(errorhandler())
}
Take your applications from the previous activity and add in morgan
and errorhandler
for logging!