Connect’s built-in middleware - Web application development with Node - Node.js in Action (2014)

Node.js in Action (2014)

Part 2. Web application development with Node

Chapter 7. Connect’s built-in middleware

This chapter covers

· Middleware for parsing cookies, request bodies, and query strings

· Middleware that implements core web application needs

· Middleware that handles web application security

· Middleware for serving static files

In the previous chapter, you learned what middleware is, how to create it, and how to use it with Connect. But Connect’s real power comes from its bundled middleware, which meets many common web application needs, such as session management, cookie parsing, body parsing, request logging, and much more. This middleware ranges in complexity and provides a great starting point for building simple web servers or higher-level web frameworks.

Throughout this chapter, we’ll explain and demonstrate the more commonly used bundled middleware components. Table 7.1 provides an overview of the middleware we’ll cover.

Table 7.1. Connect middleware quick reference guide

Middleware component

Section

Description

cookieParser()

7.1.1

Provides req.cookies and req.signedCookies for subsequent middleware to use.

bodyParser()

7.1.2

Provides req.body and req.files for subsequent middleware to use.

limit()

7.1.3

Restricts request body sizes based on a given byte length limit. Must go before the bodyParser middleware component.

query()

7.1.4

Provides req.query for subsequent middleware to use.

logger()

7.2.1

Logs configurable information about incoming HTTP requests to a stream, like stdout or a log file.

favicon()

7.2.2

Responds to /favicon.ico HTTP requests. Usually placed before the logger middleware component so that you don’t have to see it in your log files.

methodOverride()

7.2.3

Allows you to fake req.method for browsers that can’t use the proper method. Depends on bodyParser.

vhost()

7.2.4

Uses a given middleware component and/or HTTP server instances based on a specified hostname (such as nodejs.org).

session()

7.2.5

Sets up an HTTP session for a user and provides a persistent req.session object in between requests. Depends on cookieParser.

basicAuth()

7.3.1

Provides HTTP Basic authentication for your application.

csrf()

7.3.2

Protects against cross-site request forgery attacks in HTTP forms. Depends on session.

errorHandler()

7.3.3

Returns stack traces to the client when a server-side error occurs. Useful for development; don’t use for production.

static()

7.4.1

Serves files from a given directory to HTTP clients. Works really well with Connect’s mounting feature.

compress()

7.4.2

Optimizes HTTP responses using gzip compression.

directory()

7.4.3

Serves directory listings to HTTP clients, providing the optimal result based on the client’s Accept request header (plain text, JSON, or HTML).

First up, we’ll look at middleware that implements the various parsers needed to build proper web applications, because these are the foundation for most of the other middleware.

7.1. Middleware for parsing cookies, request bodies, and query strings

Node’s core doesn’t provide modules for higher-level web application concepts like parsing cookies, buffering request bodies, or parsing complex query strings, so Connect provides those out of the box for your application to use. In this section, we’ll cover the four built-in middleware components that parse request data:

· cookieParser()—Parses cookies from web browsers into req.cookies

· bodyParser()—Consumes and parses the request body into req.body

· limit()—Goes hand in hand with bodyParser() to keep requests from getting too big

· query()—Parses the request URL query string into req.query

Let’s start off with cookies, which are often used by web browsers to simulate state because HTTP is a stateless protocol.

7.1.1. cookieParser(): parsing HTTP cookies

Connect’s cookie parser supports regular cookies, signed cookies, and special JSON cookies out of the box. By default, regular unsigned cookies are used, populating the req.cookies object. But if you want signed cookie support, which is required by the session() middleware, you’ll want to pass a secret string when creating the cookie-Parser() instance.

Setting Cookies on the Server Side

The cookieParser() middleware doesn’t provide any helpers for setting outgoing cookies. For this, you should use the res.setHeader() function with Set-Cookie as the header name. Connect patches Node’s default res.setHeader() function to special-case the Set-Cookieheaders so that it just works, as you’d expect it to.

Basic usage

The secret passed as the argument to cookieParser() is used to sign and unsign cookies, allowing Connect to determine whether the cookies’ contents have been tampered with (because only your application knows the secret’s value). Typically the secret should be a reasonably large string, potentially randomly generated.

In the following example, the secret is tobi is a cool ferret:

var connect = require('connect');

var app = connect()

.use(connect.cookieParser('tobi is a cool ferret'))

.use(function(req, res){

console.log(req.cookies);

console.log(req.signedCookies);

res.end('hello\n');

}).listen(3000);

The req.cookies and req.signedCookies properties get set to objects representing the parsed Cookie header that was sent with the request. If no cookies are sent with the request, the objects will both be empty.

Regular cookies

If you were to fire some HTTP requests off to the preceding server using curl(1) without the Cookie header field, both of the console.log() calls would output an empty object:

$ curl http://localhost:3000/

{}

{}

Now try sending a few cookies. You’ll see that both cookies are available as properties of req.cookies:

$ curl http://localhost:3000/ -H "Cookie: foo=bar, bar=baz"

{ foo: 'bar', bar: 'baz' }

{}

Signed cookies

Signed cookies are better suited for sensitive data, as the integrity of the cookie data can be verified, helping to prevent man-in-the-middle attacks. Signed cookies are placed in the req.signedCookies object when valid. The reasoning behind having two separate objects is that it shows the developer’s intention. If you were to place both signed and unsigned cookies in the same object, a regular cookie could be crafted to contain data to mimic a signed cookie.

A signed cookie looks something like tobi.DDm3AcVxE9oneYnbmpqxoyhyKsk, where the content to the left of the period (.) is the cookie’s value, and the content to the right is the secret hash generated on the server with SHA-1 HMAC (hash-based message authentication code). When Connect attempts to unsign the cookie, it will fail if either the value or HMAC has been altered.

Suppose, for example, you set a signed cookie with a key of name and a value of luna. cookieParser would encode the cookie to luna.PQLM0wNvqOQEObZXUkWbS5m6Wlg. The hash portion is checked on each request, and when the cookie is sent intact, it will be available asreq.signedCookies.name:

$ curl http://localhost:3000/ -H "Cookie:

name=luna.PQLM0wNvqOQEObZXUkWbS5m6Wlg"

{}

{ name: 'luna' }

GET / 200 4ms

If the cookie’s value were to change, as shown in the next curl command, the name cookie would be available as req.cookies.name because it wasn’t valid. It might still be of use for debugging or application-specific purposes:

$ curl http://localhost:3000/ -H "Cookie:

name=manny.PQLM0wNvqOQEObZXUkWbS5m6Wlg"

{ name: 'manny.PQLM0wNvqOQEObZXUkWbS5m6Wlg' }

{}

GET / 200 1ms

JSON Cookies

The special JSON cookie is prefixed with j:, which informs Connect that it is intended to be serialized JSON. JSON cookies can be either signed or unsigned.

Frameworks such as Express can use this functionality to provide developers with a more intuitive cookie interface, instead of requiring them to manually serialize and parse JSON cookie values. Here’s an example of how Connect parses JSON cookies:

$ curl http://localhost:3000/ -H 'Cookie: foo=bar,

bar=j:{"foo":"bar"}'

{ foo: 'bar', bar: { foo: 'bar' } }

{}

GET / 200 1ms

As mentioned, JSON cookies can also be signed, as illustrated in the following request:

$ curl http://localhost:3000/ -H "Cookie:

cart=j:{\"items\":[1]}.sD5p6xFFBO/4ketA1OP43bcjS3Y"

{}

{ cart: { items: [ 1 ] } }

GET / 200 1ms

Setting Outgoing Cookies

As noted earlier, the cookieParser() middleware doesn’t provide any functionality for writing outgoing headers to the HTTP client via the Set-Cookie header. Connect, however, provides explicit support for multiple Set-Cookie headers via the res.setHeader() function.

Say you wanted to set a cookie named foo with the string value bar. Connect enables you to do this in one line of code by calling res.setHeader(). You can also set the various options of a cookie, like its expiration date, as shown in the second setHeader() call here:

var connect = require('connect');

var app = connect()

.use(function(req, res){

res.setHeader('Set-Cookie', 'foo=bar');

res.setHeader('Set-Cookie', 'tobi=ferret;

Expires=Tue, 08 Jun 2021 10:18:14 GMT');

res.end();

}).listen(3000);

If you check out the headers that this server sends back to the HTTP request by using the --head flag of curl, you can see the Set-Cookie headers set as you would expect:

$ curl http://localhost:3000/ --head

HTTP/1.1 200 OK

Set-Cookie: foo=bar

Set-Cookie: tobi=ferret; Expires=Tue, 08 Jun 2021 10:18:14 GMT

Connection: keep-alive

That’s all there is to sending cookies with your HTTP response. You can store any kind of text data in cookies, but it has become usual to store a single session cookie on the client side so that you can have full user state on the server. This session technique is encapsulated in the session()middleware, which you’ll learn about a little later in this chapter.

Another extremely common need in web application development is parsing incoming request bodies. Next we’ll look at the bodyParser() middleware and how it will make your life as a Node developer easier.

7.1.2. bodyParser(): parsing request bodies

A common need for all kinds of web applications is accepting input from the user. Let’s say you wanted to accept file uploads using the <input type="file"> HTML tag. One line of code adding the bodyParser() middleware component is all it takes. This is an extremely helpful component, and it’s actually an aggregate of three other smaller components: json(), urlencoded(), and multipart().

The bodyParser() component provides a req.body property for your application to use by parsing JSON, x-www-form-urlencoded, and multipart/form-data requests. When the request is a multipart/form-data request, like a file upload, the req.files object will also be available.

Basic Usage

Suppose you want to accept registration information for your application though a JSON request. All you have to do is add the bodyParser() component before any other middleware that will access the req.body object. Optionally, you can pass in an options object that will be passed through to the subcomponents mentioned previously (json(), urlencoded(), and multipart()):

var app = connect()

.use(connect.bodyParser())

.use(function(req, res){

// .. do stuff to register the user ..

res.end('Registered new user: ' + req.body.username);

});

Parsing JSON Data

The following curl(1) request could be used to submit data to your application, sending a JSON object with the username property set to tobi:

$ curl -d '{"username":"tobi"}' -H "Content-Type: application/json"

http://localhost

Registered new user: tobi

Parsing Regular <Form> Data

Because bodyParser() parses data based on the Content-Type, the input format is abstracted, so that all your application needs to care about is the resulting req.body data object.

For example, the following curl(1) command will send x-www-form-urlencoded data, but the middleware will work as expected without any additional changes to the code. It will provide the req.body.name property just as before:

$ curl -d name=tobi http://localhost

Registered new user: tobi

Parsing Multipart <Form> Data

The bodyParser parses multipart/form-data, typical for file uploads. It’s backed by the third-party module formidable, discussed earlier in chapter 4.

To test this functionality, you can log both the req.body and req.files objects to inspect them:

var app = connect()

.use(connect.bodyParser())

.use(function(req, res){

console.log(req.body);

console.log(req.files);

res.end('thanks!');

});

Now you can simulate a browser file upload using curl(1) with the -F or --form flag, which expects the name of the field and the value. The following example will upload a single image named photo.png, as well as the field name containing tobi:

$ curl -F image=@photo.png -F name=tobi http://localhost

thanks!

If you take a look at the output of the application, you’ll see something very similar to the following example output, where the first object represents req.body and the second is req.files. As you can see in the output, req.files.image.path would be available to your application, and you could rename the file on disk, transfer the data to a worker for processing, upload to a content delivery network, or do anything else your app requires:

{ name: 'tobi' }

{ image:

{ size: 4,

path: '/tmp/95cd49f7ea6b909250abbd08ea954093',

name: 'photo.png',

type: 'application/octet-stream',

lastModifiedDate: Sun, 11 Dec 2011 20:52:20 GMT,

length: [Getter],

filename: [Getter],

mime: [Getter] } }

Now that we’ve looked at the body parsers, you may be wondering about security. If bodyParser() buffers the json and x-www-form-urlencoded request bodies in memory, producing one large string, couldn’t an attacker produce extremely large bodies of JSON to deny service to valid visitors? The answer to that is essentially yes, and this is why the limit() middleware component exists. It allows you to specify what an acceptable request body size is. Let’s take a look.

7.1.3. limit(): request body limiting

Simply parsing request bodies is not enough. Developers also need to properly classify acceptable requests and place limits on them when appropriate. The limit() middleware component is designed to help filter out huge requests, whether they are intended to be malicious or not.

For example, an innocent user uploading a photo may accidentally send an uncompressed RAW image consisting of several hundred megabytes, or a malicious user may craft a massive JSON string to lock up bodyParser(), and in turn V8’s JSON.parse() method. You must configure your server to handle these situations.

Why is limit() needed?

Let’s take a look at how a malicious user can render a vulnerable server useless. First, create the following small Connect application named server.js, which does nothing other than parse request bodies using the bodyParser() middleware component:

var connect = require('connect');

var app = connect()

.use(connect.bodyParser());

app.listen(3000);

Now create a file named dos.js, as shown in the following listing. You can see how a malicious user could use Node’s HTTP client to attack the preceding Connect application, simply by writing several megabytes of JSON data.

Listing 7.1. Performing a denial of service attack on a vulnerable HTTP server

Fire up the server and run the attack script:

$ node server.js &

$ node dos.js

You’ll see that it can take V8 up to 10 seconds (depending on your hardware) to parse such a large JSON string. This is bad, but thankfully it’s exactly what the limit() middleware component was designed to prevent.

Basic Usage

By adding the limit() component before bodyParser(), you can specify a maximum size for the request body either by the number of bytes (like 1024) or by using a string representation in any of the following ways: 1gb, 25mb, or 50kb.

If you set limit() to 32kb and run the server and attack script again, you’ll see that Connect will terminate the request at 32 kilobytes:

var app = connect()

.use(connect.limit('32kb'))

.use(connect.bodyParser())

.use(hello);

http.createServer(app).listen(3000);

Wrapping Limit() for Greater Flexibility

Limiting every request body to a small size like 32kb is not feasible for applications accepting user uploads, because most image uploads will be larger than this, and files such as videos will definitely be much larger. But it may be a reasonable size for bodies formatted as JSON or XML, for example.

A good idea for applications needing to accept varying sizes of request bodies would be to wrap the limit() middleware component in a function based on some type of configuration. For example, you could wrap the component to specify a Content-Type, as shown in the following listing.

Listing 7.2. Limiting body size based on a request’s Content-Type

Another way to use this middleware would be to provide the limit option to bodyParser(), and the latter could call limit() transparently.

The next middleware component we’ll cover is a small, but very useful, component that parses the request’s query strings for your application to use.

7.1.4. query(): query-string parser

You’ve already learned about bodyParser(), which can parse POST form requests, but what about the GET form requests? That’s where the query() middleware component comes in. It parses the query string, when one is present, and provides the req.query object for your application to use. For developers coming from PHP, this is similar to the $_GET associative array. Much like bodyParser(), query() should be placed before any middleware that will use it.

Basic usage

The following application utilizes the query() middleware component, which will respond with a JSON representation of the query string sent by the request. Query-string parameters are usually used for controlling the display of the data being sent back:

var app = connect()

.use(connect.query())

.use(function(req, res, next){

res.setHeader('Content-Type', 'application/json');

res.end(JSON.stringify(req.query));

});

Suppose you were designing a music library app. You could offer a search engine and use the query string to build up the search parameters, something like this: /songSearch?artist=Bob%20Marley&track=Jammin. This example query would produce a res.query object like this:

{ artist: 'Bob Marley', track: 'Jammin' }

The query() component uses the same third-party qs module as bodyParser(), so complex query strings like ?images[]=foo.png&images[]=bar.png produce the following object:

{ images: [ 'foo.png', 'bar.png' ] }

When no query-string parameters are given in the HTTP request, like /songSearch, then req.query will default to an empty object:

{}

That’s all there is to it. Next we’ll look at the built-in middleware that covers core web application needs, such as logging and sessions.

7.2. Middleware that implements core web application functions

Connect aims to implement and provide built-in middleware for the most common web application needs, so that they don’t need to be re-implemented over and over by every developer. Core web application functions like logging, sessions, and virtual hosting are all provided by Connect out of the box.

In this section, you’ll learn about five very useful middleware components that you’ll likely use in your applications:

· logger()—Provides flexible request logging

· favicon()—Takes care of the /favicon.ico request without you having to think about it

· methodOverride()—Enables incapable clients to transparently overwrite req.method

· vhost()—Sets up multiple websites on a single server (virtual hosting)

· session()—Manages session data

Up until now you’ve created your own custom logging middleware, but Connect provides a very flexible solution named logger(), so let’s explore that first.

7.2.1. logger(): logging requests

logger() is a flexible request-logging middleware component with customizable log formats. It also has options for buffering log output to decrease disk writes, and for specifying a log stream if you want to log to something other than the console, such as a file or socket.

Basic usage

To use Connect’s logger() component in your own application, invoke it as a function to return a logger() middleware instance, as shown in the following listing.

Listing 7.3. Using the logger() middleware component

By default, the logger uses the following format, which is extremely verbose, but it provides useful information about each HTTP request. This is similar to how other web servers, such as Apache, create their log files:

':remote-addr - - [:date] ":method :url HTTP/:http-version" :status

:res[content-length] ":referrer" ":user-agent"'

Each of the :something pieces are tokens, and in an actual log entry they’d contain real values from the HTTP request that’s being logged. For example, a simple curl(1) request would generate a log line similar to the following:

127.0.0.1 - - [Wed, 28 Sep 2011 04:27:07 GMT]

"GET / HTTP/1.1" 200 - "-"

"curl/7.19.7 (universal-apple-darwin10.0)

libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3"

Customizing log formats

The most basic use of logger() doesn’t require any customization. But you may want a custom format that records other information, or that’s less verbose, or that provides custom output. To customize the log format, you pass a custom string of tokens. For example, the following format would output something like GET /users 15 ms:

var app = connect()

.use(connect.logger(':method :url :response-time ms'))

.use(hello);

By default, the following tokens are available for use (note that the header names are not case-sensitive):

· :req[header] ex: :req[Accept]

· :res[header] ex: :res[Content-Length]

· :http-version

· :response-time

· :remote-addr

· :date

· :method

· :url

· :referrer

· :user-agent

· :status

Defining custom tokens is easy. All you have to do is provide a token name and callback function to the connect.logger.token function. For example, say you wanted to log each request’s query string. You might define it like this:

var url = require('url');

connect.logger.token('query-string', function(req, res){

return url.parse(req.url).query;

});

logger() also comes with other predefined formats than the default one, such as short and tiny. Another predefined format is dev, which produces concise output for development, for situations when you’re usually the only user on the site and you don’t care about the details of the HTTP requests. This format also color-codes the response status codes by type: responses with a status code in the 200s are green, 300s are blue, 400s are yellow, and 500s are red. This color scheme makes it great for development.

To use a predefined format, you simply provide the name to logger():

var app = connect()

.use(connect.logger('dev'))

.use(hello);

Now that you know how to format the logger’s output, let’s take a look at the options you can provide to it.

Logger options: stream, immediate, and buffer

As mentioned previously, you can use options to tweak how logger() behaves.

One such option is stream, which allows you to pass a Node Stream instance that the logger will write to instead of stdout. This would allow you to direct the logger output to its own log file, independent of your server’s own output using a Stream instance created fromfs.createWriteStream.

When you use these options, it’s generally recommended to also include the format property. The following example uses a custom format and logs to /var/log/myapp.log with the append flag, so that the file isn’t truncated when the application boots:

var fs = require('fs')

var log = fs.createWriteStream('/var/log/myapp.log', { flags: 'a' })

var app = connect()

.use(connect.logger({ format: ':method :url', stream: log }))

.use('/error', error)

.use(hello);

Another useful option is immediate, which writes the log line when the request is first received, rather than waiting for the response. You might use this option if you’re writing a server that keeps its requests open for a long time, and you want to know when the connection begins. Or you might use it for debugging a critical section of your app. This means that tokens such as :status and :response-time can’t be used, because they’re related to the response. To enable immediate mode, pass true for the immediate value, as shown here:

var app = connect()

.use(connect.logger({ immediate: true }))

.use('/error', error)

.use(hello);

The third option available is buffer, which is useful when you want to minimize the number of writes to the disk where your log file resides. This is especially useful if your log file is being written over a network, and you want to minimize the amount of network activity. The bufferoption takes a numeric value specifying the interval in milliseconds between flushes of the buffer, or you can just pass true to use the default interval.

That’s it for logging! Next we’ll look at the favicon-serving middleware component.

7.2.2. favicon(): serving a favicon

A favicon is that tiny website icon your browser displays in the address bar and bookmarks. To get this icon, the browser makes a request for a file at /favicon.ico. It’s usually best to serve favicon files as soon as possible, so the rest of your application can simply ignore them. Thefavicon() middleware component will serve Connect’s favicon by default (when no arguments are passed to it). This favicon is shown in figure 7.1.

Figure 7.1. Connect’s default favicon

Basic usage

Typically favicon() is used at the very top of the stack, so even logging is ignored for favicon requests. The icon is then cached in memory for fast subsequent responses.

The following example shows favicon() requesting a custom .ico file by passing the file path as the only argument:

connect()

.use(connect.favicon(__dirname + '/public/favicon.ico'))

.use(connect.logger())

.use(function(req, res) {

res.end('Hello World!\n');

});

Optionally, you can pass in a maxAge argument to specify how long browsers should cache the favicon in memory.

Next we have another small but helpful middleware component: method-Override(). It provides the means to fake the HTTP request method when client capabilities are limited.

7.2.3. methodOverride(): faking HTTP methods

An interesting problem arises in the browser when you’re building a server that utilizes special HTTP verbs, like PUT or DELETE. The browser <form> methods can only be GET or POST, restricting you from using any other methods in your application.

A common workaround is to add an <input type=hidden> with the value set to the method name you want to use, and then have the server check that value and “pretend” it’s the request method for this request. The methodOverride() middleware component is the server-side half of this technique.

Basic usage

By default, the HTML input name is _method, but you can pass a custom value to methodOverride(), as shown in the following snippet:

connect()

.use(connect.methodOverride('__method__'))

.listen(3000)

To demonstrate how methodOverride() is implemented, let’s create a tiny application to update user information. The application will consist of a single form that will respond with a simple success message when the form is submitted by the browser and processed by the server, as illustrated in figure 7.2.

Figure 7.2. Using methodOverride() to simulate a PUT request to update a form in the browser

The application updates the user data through the use of two separate middleware components. In the update function, next() is called when the request method is not PUT. As mentioned previously, most browsers don’t respect the form attribute method="put", so the application in the following listing won’t function properly.

Listing 7.4. A broken user-update application

var connect = require('connect');

function edit(req, res, next) {

if ('GET' != req.method) return next();

res.setHeader('Content-Type', 'text/html');

res.write('<form method="put">');

res.write('<input type="text" name="user[name]" value="Tobi" />');

res.write('<input type="submit" value="Update" />');

res.write('</form>');

res.end();

}

function update(req, res, next) {

if ('PUT' != req.method) return next();

res.end('Updated name to ' + req.body.user.name);

}

var app = connect()

.use(connect.logger('dev'))

.use(connect.bodyParser())

.use(edit)

.use(update);

app.listen(3000);

The update application needs to look something like listing 7.5. Here an additional input with the name _method has been added to the form, and methodOverride() has been added below the bodyParser() component because it references req.body to access the form data.

Listing 7.5. A user-update application with methodOverride() implemented

var connect = require('connect');

function edit(req, res, next) {

if ('GET' != req.method) return next();

res.setHeader('Content-Type', 'text/html');

res.write('<form method="post">');

res.write('<input type="hidden" name="_method" value="put" />');

res.write('<input type="text" name="user[name]" value="Tobi" />');

res.write('<input type="submit" value="Update" />');

res.write('</form>');

res.end();

}

function update(req, res, next) {

if ('PUT' != req.method) return next();

res.end('Updated name to ' + req.body.user.name);

}

var app = connect()

.use(connect.logger('dev'))

.use(connect.bodyParser())

.use(connect.methodOverride())

.use(edit)

.use(update)

.listen(3000);

Accessing the original req.method

methodOverride() alters the original req.method property, but Connect copies over the original method, which you can always access with req.originalMethod. This means the previous form would output values like these:

console.log(req.method);

// "PUT"

console.log(req.originalMethod);

// "POST"

This may seem like quite a bit of work for a simple form, but we promise this will be more enjoyable when we discuss higher-level features from Express in chapter 8 and templating in chapter 11.

The next thing we’ll look at is vhost(), which is a small middleware component for serving applications based on hostnames.

7.2.4. vhost(): virtual hosting

The vhost() (virtual host) middleware component is a simple, lightweight way to route requests via the Host request header. This task is commonly performed by a reverse proxy, which then forwards the request to a web server running locally on a different port. The vhost() component does this in the same Node process by passing control to a Node HTTP server associated with the vhost instance.

Basic usage

Like all the middleware that Connect provides out of the box, a single line is all it takes to get up and running with the vhost() component. It takes two arguments: The first is the hostname string that this vhost instance will match against. The second is the http.Server instance that will be used when an HTTP request with a matching hostname is made (all Connect apps are subclasses of http.Server, so an application instance will work as well).

var connect = require('connect');

var server = connect()

var app = require('./sites/expressjs.dev');

server.use(connect.vhost('expressjs.dev', app));

server.listen(3000);

In order to use the preceding ./sites/expressjs.dev module, it should assign the HTTP server to module.exports as in the following example:

var http = require('http')

module.exports = http.createServer(function(req, res){

res.end('hello from expressjs.com\n');

});

Using multiple vhost() instances

Like any other middleware, you can use vhost() more than once in an application to map several hosts to their associated applications:

var app = require('./sites/expressjs.dev');

server.use(connect.vhost('expressjs.dev', app));

var app = require('./sites/learnboost.dev');

server.use(connect.vhost('learnboost.dev', app));

Rather than setting up the vhost() middleware manually like this, you could generate a list of hosts from the filesystem. That’s shown in the following example, with the fs.readdirSync() method returning an array of directory entries:

var connect = require('connect')

var fs = require('fs');

var app = connect()

var sites = fs.readdirSync('source/sites');

sites.forEach(function(site){

console.log(' ... %s', site);

app.use(connect.vhost(site, require('./sites/' + site)));

});

app.listen(3000);

The benefit of using vhost() instead of a reverse proxy is simplicity. It allows you to manage all of your applications as a single unit. This is ideal for serving several smaller sites, or for serving sites that are largely comprised of static content, but it also has the downside that if one site causes a crash, all of your sites will be taken down (because they all run in the same process).

Next we’ll take a look at one of the most fundamental middleware components that Connect provides: the session management component appropriately named session(), which relies on cookieParser() for cookie signing.

7.2.5. session(): session management

In chapter 4, we explained that Node provides all the means to implement concepts like sessions, but it doesn’t provide them out of the box. Following Node’s general philosophy of having a small core and a large user-land, session management has been left to be created as a third-party add-on to Node. And that’s exactly what the session() middleware component is for.

Connect’s session() component provides robust, intuitive, and community-backed session management with numerous session stores ranging from the default memory store to stores based on Redis, MongoDB, CouchDB, and cookies. In this section we’ll look at setting up the middleware, working with session data, and utilizing the Redis key/value store as an alternative session store.

First let’s set up the middleware and explore the options available.

Basic usage

As previously mentioned, the session() middleware component requires signed cookies to function, so you should use cookieParser() somewhere above it and pass a secret.

Listing 7.6 implements a small pageview count application with minimal setup, where no options are passed to session() at all and the default in-memory data store is used. By default, the cookie name is connect.sid and it’s set to be httpOnly, meaning client-side scripts can’t access its value. But these are options you can tweak, as you’ll soon see.

Listing 7.6. A Connect pageview counter using sessions

var connect = require('connect');

var app = connect()

.use(connect.favicon())

.use(connect.cookieParser('keyboard cat'))

.use(connect.session())

.use(function(req, res, next){

var sess = req.session;

if (sess.views) {

res.setHeader('Content-Type', 'text/html');

res.write('<p>views: ' + sess.views + '</p>');

res.end();

sess.views++;

} else {

sess.views = 1;

res.end('welcome to the session demo. refresh!');

}

});

app.listen(3000);

Setting the session expiration date

Suppose you want sessions to expire in 24 hours, to send the session cookie only when HTTPS is used, and to configure the cookie name. You might pass an object like the one shown here:

var hour = 3600000;

var sessionOpts = {

key: 'myapp_sid',

cookie: { maxAge: hour * 24, secure: true }

};

...

.use(connect.cookieParser('keyboard cat'))

.use(connect.session(sessionOpts))

...

When using Connect (and, as you’ll see in the next chapter, “Express”) you’ll often set maxAge, specifying a number of milliseconds from that point in time. This method of expressing future dates is often written more intuitively, essentially expanding to new Date(Date.now() + maxAge).

Now that sessions are set up, let’s look at the methods and properties available when working with session data.

Working with session data

Connect’s session data management is very simple. The basic principle is that any properties assigned to the req.session object are saved when the request is complete; then they’re loaded on subsequent requests from the same user (browser). For example, saving shopping cart information is as simple as assigning an object to the cart property, as shown here:

req.session.cart = { items: [1,2,3] };

When you access req.session.cart on subsequent requests, the .items array will be available. Because this is a regular JavaScript object, you can call methods on the nested objects in subsequent requests, as in the following example, and they’ll be saved as you expect:

req.session.cart.items.push(4);

One important thing to keep in mind is that this session object gets serialized as JSON in between requests, so the req.session object has the same restrictions as JSON: cyclic properties aren’t allowed, function objects can’t be used, Date objects can’t be serialized correctly, and so on. Keep those restrictions in mind when using the session object.

Connect will save session data for you automatically, but internally it’s calling the Session#save([callback]) method, which is also available as a public API. Two additional helpful methods are Session#destroy() and Session#regenerate(), which are often used when authenticating a user to prevent session fixation attacks. When you build applications with Express in later chapters, you’ll use these methods for authentication.

Now let’s move on to manipulating session cookies.

Manipulating session cookies

Connect allows you to provide global cookie settings for sessions, but it’s also possible to manipulate a specific cookie via the Session#cookie object, which defaults to the global settings.

Before you start tweaking properties, let’s extend the previous session application to inspect the session cookie properties by writing each property into individual <p> tags in the response HTML, as shown here:

...

res.write('<p>views: ' + sess.views + '</p>');

res.write('<p>expires in: ' + (sess.cookie.maxAge / 1000) + 's</p>');

res.write('<p>httpOnly: ' + sess.cookie.httpOnly + '</p>');

res.write('<p>path: ' + sess.cookie.path + '</p>');

res.write('<p>domain: ' + sess.cookie.domain + '</p>');

res.write('<p>secure: ' + sess.cookie.secure + '</p>');

...

Connect allows all of the cookie properties, such as expires, httpOnly, secure, path, and domain, to be altered programmatically on a per-session basis. For example, you could expire an active session in 5 seconds like this:

req.session.cookie.expires = new Date(Date.now() + 5000);

An alternative, more intuitive API for expiry is the .maxAge accessor, which allows you to get and set the value in milliseconds relative to the current time. The following will also expire the session in 5 seconds:

req.session.cookie.maxAge = 5000;

The remaining properties, domain, path, and secure, limit the cookie scope, restricting it by domain, path, or to secure connections, whereas httpOnly prevents client-side scripts from accessing the cookie data. These properties can be manipulated in the same manner:

req.session.cookie.path = '/admin';

req.session.cookie.httpOnly = false;

So far you’ve been using the default memory store to store session data, so let’s take a look at how you can plug in alternative data stores.

Session stores

The built-in connect.session.MemoryStore is a simple, in-memory data store, which is ideal for running application tests because no other dependencies are necessary. But during development and in production, it’s best to have a persistent, scalable database backing your session data.

Just about any database can act as a session store, but low-latency key/value stores work best for such volatile data. The Connect community has created several session stores for databases, including CouchDB, MongoDB, Redis, Memcached, PostgreSQL, and others.

Here you’ll use Redis with the connect-redis module. In chapter 5 you learned about interacting with Redis using the node_redis module. Now you’ll learn how to use Redis to store your session data in Connect. Redis is a good backing store because it supports key expiration, it provides great performance, and it’s easy to install.

You should have Redis installed and running from chapter 5, but try invoking the redis-server command just to be sure:

$ redis-server

[11790] 16 Oct 16:11:54 * Server started, Redis version 2.0.4

[11790] 16 Oct 16:11:54 * DB loaded from disk: 0 seconds

[11790] 16 Oct 16:11:54 * The server is now ready to accept

connections on port 6379

[11790] 16 Oct 16:11:55 - DB 0: 522 keys (0 volatile) in 1536 slots HT.

Next, you need to install connect-redis by adding it to your package.json file and running npm install, or by executing npm install connect-redis directly. The connect-redis module exports a function that should be passed connect, as shown here:

var connect = require('connect')

var RedisStore = require('connect-redis')(connect);

var app = connect()

.use(connect.favicon())

.use(connect.cookieParser('keyboard cat'))

.use(connect.session({ store: new RedisStore({ prefix: 'sid' }) }))

...

Passing the connect reference to connect-redis allows it to inherit from connect .session.Store.prototype. This is important because in Node a single process may use multiple versions of a module simultaneously; by passing your specific version of Connect, you can be sure that connect-redis uses the proper copy.

The instance of RedisStore is passed to session() as the store value, and any options you want to use, such as a key prefix for your sessions, can be passed to the RedisStore constructor.

Whew! session was a lot to cover, but that finishes up all the core concept middleware. Next we’ll go over the built-in middleware that handles web application security. This is a very important subject for applications needing to secure their data.

7.3. Middleware that handles web application security

As we’ve stated many times, Node’s core API is intentionally low-level. This means it provides no built-in security or best practices when it comes to building web applications. Fortunately, Connect steps in to implement these security practices for use in your Connect applications.

This section will teach you about three more of Connect’s built-in middleware components, this time with a focus on security:

· basicAuth()—Provides HTTP Basic authentication for protecting data

· csrf()—Implements protection against cross-site request forgery (CSRF) attacks

· errorHandler()—Helps you debug during development

First, basicAuth() implements HTTP Basic authentication for safeguarding restricted areas of your application.

7.3.1. basicAuth(): HTTP Basic authentication

In chapter 6’s section 6.4, you created a crude Basic authentication middleware component. Well, it turns out that Connect provides a real implementation of this out of the box. As previously mentioned, Basic authentication is a very simple HTTP authentication mechanism, and it should be used with caution because user credentials can be trivial for an attacker to intercept unless Basic authentication is served over HTTPS. That being said, it can be useful for adding quick and dirty authentication to a small or personal application.

When your application has the basicAuth() component in use, web browsers will prompt for credentials the first time the user attempts to connect to your application, as shown in figure 7.3.

Figure 7.3. Basic authentication prompt

Basic usage

The basicAuth() middleware component provides three means of validating credentials. The first is to pass it a single username and password, as shown here:

var app = connect()

.use(connect.basicAuth('tj', 'tobi'));

Providing a callback function

The second way of validating credentials is to pass basicAuth() a callback, which must return true in order to succeed. This is useful for checking the credentials against a hash:

var users = {

tobi: 'foo',

loki: 'bar',

jane: 'baz'

};

var app = connect()

.use(connect.basicAuth(function(user, pass){

return users[user] === pass;

});

Providing an asynchronous callback function

The final option is similar, except this time a callback is passed to basicAuth() with three arguments defined, which enables the use of asynchronous lookups. This is useful when authenticating from a file on disk, or when querying from a database.

Listing 7.7. A Connect basicAuth middleware component doing asynchronous lookups

An example with curl(1)

Suppose you want to restrict access to all requests coming to your server. You might set up the application like this:

var connect = require('connect');

var app = connect()

.use(connect.basicAuth('tobi', 'ferret'))

.use(function (req, res) {

res.end("I'm a secret\n");

});

app.listen(3000);

Now try issuing an HTTP request to the server with curl(1), and you’ll see that you’re unauthorized:

$ curl http://localhost -i

HTTP/1.1 401 Unauthorized

WWW-Authenticate: Basic realm="Authorization Required"

Connection: keep-alive

Transfer-Encoding: chunked

Unauthorized

Issuing the same request with HTTP Basic authorization credentials (notice the beginning of the URL) will provide access:

$ curl --user tobi:ferret http://localhost -i

HTTP/1.1 200 OK

Date: Sun, 16 Oct 2011 22:42:06 GMT

Cache-Control: public, max-age=0

Last-Modified: Sun, 16 Oct 2011 22:41:02 GMT

ETag: "13-1318804862000"

Content-Type: text/plain; charset=UTF-8

Accept-Ranges: bytes

Content-Length: 13

Connection: keep-alive

I'm a secret

Continuing on with the security theme of this section, let’s look at the csrf() middleware component, which is designed to help protect against cross-site request forgery attacks.

7.3.2. csrf(): cross-site request forgery protection

Cross-site request forgery (CSRF) is a form of attack that exploits the trust that a web browser has in a site. The attack works by having an authenticated user on your application visit a different site that an attacker has either created or compromised, and then making requests on the user’s behalf without them knowing about it.

This is a complicated attack, so let’s go through it with an example. Suppose that in your application the request DELETE /account will trigger a user’s account to be destroyed (though only while the user is logged in). Now suppose that user visits a forum that happens to be vulnerable to CSRF. An attacker could post a script that issues the DELETE /account request, thus destroying the user’s account. This is a bad situation for your application to be in, and the csrf() middleware component can help protect against such an attack.

The csrf() component works by generating a 24-character unique ID, the authenticity token, and assigning it to the user’s session as req.session._csrf. This token can then be included as a hidden form input named _csrf, and the CSRF component can validate the token on submission. This process is repeated for each interaction.

Basic usage

To ensure that csrf() can access req.body._csrf (the hidden input value) and req.session._csrf, you’ll want to make sure that you add the csrf() below bodyParser() and session(), as shown in the following example:

connect()

.use(connect.bodyParser())

.use(connect.cookieParser('secret'))

.use(connect.session())

.use(connect.csrf());

Another aspect of web development is ensuring verbose logs and detailed error reporting are available both in production and development environments. Let’s look at the errorHandler() middleware component, which is designed to do exactly that.

7.3.3. errorHandler(): development error handling

The errorHandler() middleware component bundled with Connect is ideal for development, providing verbose HTML, JSON, and plain-text error responses based on the Accept header field. It’s meant for use during development and shouldn’t be part of the production configuration.

Basic usage

Typically this component should be the last used so it can catch all errors:

var app = connect()

.use(connect.logger('dev'))

.use(function(req, res, next){

setTimeout(function () {

next(new Error('something broke!'));

}, 500);

})

.use(connect.errorHandler());

Receiving an HTML error response

If you view any page in your browser with the setup shown here, you’ll see a Connect error page like the one shown in figure 7.4, displaying the error message, the response status, and the entire stack trace.

Figure 7.4. Connect’s default errorHandler() middleware as displayed in a web browser

Receiving a plain-text error response

Now suppose you’re testing an API built with Connect. It’s far from ideal to respond with a large chunk of HTML, so by default errorHandler() will respond with text/plain, which is ideal for command-line HTTP clients such as curl(1). This is illustrated in the following stdout:

$ curl http://localhost/

Error: something broke!

at Object.handle (/Users/tj/Projects/node-in-action/source

/connect-middleware-errorHandler.js:12:10)

at next (/Users/tj/Projects/connect/lib/proto.js:179:15)

at Object.logger [as handle] (/Users/tj/Projects/connect

/lib/middleware/logger.js:155:5)

at next (/Users/tj/Projects/connect/lib/proto.js:179:15)

at Function.handle (/Users/tj/Projects/connect/lib/proto.js:192:3)

at Server.app (/Users/tj/Projects/connect/lib/connect.js:53:31)

at Server.emit (events.js:67:17)

at HTTPParser.onIncoming (http.js:1134:12)

at HTTPParser.onHeadersComplete (http.js:108:31)

at Socket.ondata (http.js:1029:22)

Receiving a JSON error response

If you send an HTTP request that has the Accept: application/json HTTP header, you’ll get the following JSON response:

$ curl http://localhost/ -H "Accept: application/json"

{"error":{"stack":"Error: something broke!\n

at Object.handle (/Users/tj/Projects/node-in-action

/source/connect-middleware-errorHandler.js:12:10)\n

at next (/Users/tj/Projects/connect/lib/proto.js:179:15)\n

at Object.logger [as handle] (/Users/tj/Projects

/connect/lib/middleware/logger.js:155:5)\n

at next (/Users/tj/Projects/connect/lib/proto.js:179:15)\n

at Function.handle (/Users/tj/Projects/connect/lib/proto.js:192:3)\n

at Server.app (/Users/tj/Projects/connect/lib/connect.js:53:31)\n

at Server.emit (events.js:67:17)\n

at HTTPParser.onIncoming (http.js:1134:12)\n

at HTTPParser.onHeadersComplete (http.js:108:31)\n

at Socket.ondata (http.js:1029:22)","message":"something broke!"}}

We’ve added additional formatting to the JSON response, so it’s easier to read on the page, but when Connect sends the JSON response, it gets compacted nicely by JSON.stringify().

Are you feeling like a Connect security guru now? Maybe not yet, but you should have enough of the basics down to make your applications secure, all using Connect’s built-in middleware. Now let’s move on to a very common web application function: serving static files.

7.4. Middleware for serving static files

Serving static files is another requirement common to many web applications that’s not provided by Node’s core. Fortunately, Connect has you covered here as well.

In this section, you’ll learn about three more of Connect’s built-in middleware components, this time focusing on serving files from the filesystem, much like regular HTTP servers do:

· static()—Serves files from the filesystem from a given root directory

· compress()—Compresses responses, ideal for use with static()

· directory()—Serves pretty directory listings when a directory is requested

First we’ll show you how to serve static files with a single line of code using the static component.

7.4.1. static(): static file serving

Connect’s static() middleware component implements a high-performance, flexible, feature-rich static file server supporting HTTP cache mechanisms, Range requests, and more. Even more important, it includes security checks for malicious paths, disallows access to hidden files (beginning with a .) by default, and rejects poison null bytes. In essence, static() is a very secure and compliant static file-serving middleware component, ensuring compatibility with the various HTTP clients out there.

Basic usage

Suppose your application follows the typical scenario of serving static assets from a directory named ./public. This can be achieved with a single line of code:

app.use(connect.static('public'));

With this configuration, static() will check for regular files that exist in ./public/ based on the request URL. If a file exists, the response’s Content-Type field value will be defaulted based on the file’s extension, and the data will be transferred. If the requested path doesn’t represent a file, the next() callback will be invoked, allowing subsequent middleware (if any) to handle the request.

To test it out, create a file named ./public/foo.js with console.log('tobi'), and issue a request to the server using curl(1) with the -i flag, telling it to print the HTTP headers. You’ll see that the HTTP cache-related header fields are set appropriately, the Content-Type reflects the .js extension, and the content is transferred:

$ curl http://localhost/foo.js -i

HTTP/1.1 200 OK

Date: Thu, 06 Oct 2011 03:06:33 GMT

Cache-Control: public, max-age=0

Last-Modified: Thu, 06 Oct 2011 03:05:51 GMT

ETag: "21-1317870351000"

Content-Type: application/javascript

Accept-Ranges: bytes

Content-Length: 21

Connection: keep-alive

console.log('tobi');

Because the request path is used as is, files nested within directories are served as you’d expect. For example, you might have a GET /javascripts/jquery.js request and a GET /stylesheets/app.css request on your server, which would serve the files ./public/javascripts/jquery.js and ./public/stylesheets/app.css, respectively.

Using static() with mounting

Sometimes applications prefix pathnames with /public, /assets, /static, and so on. With the mounting concept that Connect implements, serving static files from multiple directories is simple. Just mount the app at the location you want. As mentioned in chapter 6, the middleware itself has no knowledge that it’s mounted, because the prefix is removed.

For example, a request to GET /app/files/js/jquery.js with static() mounted at /app/files will appear to the middleware as GET /js/jquery. This works out well for the prefixing functionality because /app/files won’t be part of the file resolution:

app.use('/app/files', connect.static('public'));

The original request of GET /foo.js won’t work anymore, because the middleware isn’t invoked unless the mount point is present, but the prefixed version GET /app/files/foo.js will transfer the file:

$ curl http://localhost/foo.js

Cannot get /foo.js

$ curl http://localhost/app/files/foo.js

console.log('tobi');

Absolute vs. relative directory paths

Keep in mind that the path passed into the static() component is relative to the current working directory. That means passing in "public" as your path will essentially resolve to process.cwd() + "public".

Sometimes, though, you may want to use absolute paths when specifying the base directory, and the __dirname variable helps with that:

app.use('/app/files', connect.static(__dirname + '/public'));

Serving index.html when a directory is requested

Another useful feature of static() is its ability to serve index.html files. When a request for a directory is made and an index.html file lives in that directory, it will be served.

Now that you can serve static files with a single line of code, let’s take a look at how you can compress the response data using the compress() middleware component to decrease the amount of data being transferred.

7.4.2. compress(): compressing static files

The zlib module provides developers with mechanisms for compressing and decompressing data with gzip and deflate. Connect 2.0 and above provide zlib at the HTTP server level for compressing outgoing data with the compress() middleware component.

The compress() component autodetects accepted encodings via the Accept-Encoding header field. If this field isn’t present, the identity encoding is used, meaning the response is untouched. Otherwise, if the field contains gzip, deflate, or both, the response will be compressed.

Basic usage

You should generally add compress() high in the Connect stack, because it wraps the res.write() and res.end() methods.

In the following example, the static files served will support compression:

var connect = require('connect');

var app = connect()

.use(connect.compress())

.use(connect.static('source'));

app.listen(3000);

In the snippet that follows, a small 189-byte JavaScript file is served. By default, curl(1) doesn’t send the Accept-Encoding field, so you receive plain text:

$ curl http://localhost/script.js -i

HTTP/1.1 200 OK

Date: Sun, 16 Oct 2011 18:30:00 GMT

Cache-Control: public, max-age=0

Last-Modified: Sun, 16 Oct 2011 18:29:55 GMT

ETag: "189-1318789795000"

Content-Type: application/javascript

Accept-Ranges: bytes

Content-Length: 189

Connection: keep-alive

console.log('tobi');

console.log('loki');

console.log('jane');

console.log('tobi');

console.log('loki');

console.log('jane');

console.log('tobi');

console.log('loki');

console.log('jane');

The following curl(1) command adds the Accept-Encoding field, indicating that it’s willing to accept gzip-compressed data. As you can see, even for such a small file, the data transferred is reduced considerably because the data is quite repetitive:

$ curl http://localhost/script.js -i -H "Accept-Encoding: gzip"

HTTP/1.1 200 OK

Date: Sun, 16 Oct 2011 18:31:45 GMT

Cache-Control: public, max-age=0

Last-Modified: Sun, 16 Oct 2011 18:29:55 GMT

ETag: "189-1318789795000"

Content-Type: application/javascript

Accept-Ranges: bytes

Content-Encoding: gzip

Vary: Accept-Encoding

Connection: keep-alive

Transfer-Encoding: chunked

K??+??I???O?P/?O?T??JF?????J?K???v?!?_?

You could try the same example with Accept-Encoding: deflate.

Using a custom filter function

By default, compress() supports the MIME types text/*, */json, and */javascript, as defined in the default filter function:

exports.filter = function(req, res){

var type = res.getHeader('Content-Type') || '';

return type.match(/json|text|javascript/);

};

To alter this behavior, you can pass a filter in the options object, as shown in the following snippet, which will only compress plain text:

function filter(req) {

var type = req.getHeader('Content-Type') || '';

return 0 == type.indexOf('text/plain');

}

connect()

.use(connect.compress({ filter: filter }))

Specifying compression and memory levels

Node’s zlib bindings provide options for tweaking performance and compression characteristics, and they can also be passed to the compress() function.

In the following example, the compression level is set to 3 for less but faster compression, and memLevel is set to 8 for faster compression by using more memory. These values depend entirely on your application and the resources available to it. Consult Node’s zlib documentation for details:

connect()

.use(connect.compress({ level: 3, memLevel: 8 }))

Next is the directory() middleware component, which helps static() to serve directory listings in all kinds of formats.

7.4.3. directory(): directory listings

Connect’s directory() is a small directory-listing middleware component that provides a way for users to browse remote files. Figure 7.5 illustrates the interface provided by this component, complete with a search input field, file icons, and clickable breadcrumbs.

Figure 7.5. Serving directory listings with Connect’s directory() middleware component

Basic usage

This component is designed to work with static(), which will perform the actual file serving; directory() simply serves the listings. The setup can be as simple as the following snippet, where the request GET / serves the ./public directory:

var connect = require('connect');

var app = connect()

.use(connect.directory('public'))

.use(connect.static('public'));

app.listen(3000);

Using directory() with mounting

Through the use of middleware mounting, you can prefix both the directory() and static() middleware components to any path you like, such as GET /files in the following example. Here the icons option is used to enable icons, and hidden is enabled for both components to allow the viewing and serving of hidden files:

var app = connect()

.use('/files', connect.directory('public',

{ icons: true, hidden: true }))

.use('/files', connect.static('public', { hidden: true }));

app.listen(3000);

It’s now possible to navigate through files and directories with ease.

7.5. Summary

The real power of Connect comes from its rich suite of bundled reusable middleware, which provides implementations for common web application functions like session management, robust static file serving, and compression of outgoing data, among others. Connect’s goal is to give developers some functionality right out of the box, so that everyone isn’t constantly rewriting the same pieces of code (possibly less efficiently) for their own applications or frameworks.

Connect is perfectly capable when used for building entire web applications using combinations of middleware, as you’ve seen throughout this chapter. But Connect is typically used as a building block for higher-level frameworks; for example, it doesn’t provide any routing or templating helpers. This low-level approach makes Connect great as a base for higher-level frameworks, which is exactly how Express integrates with it.

You might be thinking, why not just use Connect for building a web application? That’s perfectly possible, but the higher-level Express web framework makes full use of Connect’s functionality, while taking application development one step further. Express makes application development quicker and more enjoyable with an elegant view system, powerful routing, and several request- and response-related methods. We’ll explore Express in the next chapter.