Skip to content

Instantly share code, notes, and snippets.

@zcaceres
Last active May 26, 2022 08:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zcaceres/23d9d6eb6eb74947bb05ab4d3c996499 to your computer and use it in GitHub Desktop.
Save zcaceres/23d9d6eb6eb74947bb05ab4d3c996499 to your computer and use it in GitHub Desktop.
Express.js – A Gentle Overview

Express.js – A Gentle Overview

Zach Caceres

Express.js is a Javascript library that handles web routing and HTTP requests for web applications.

Express builds on the native HTTP library in node.js to allow for a simple, object-oriented approach to routing your web application.

The creators of Express.js describe it as a 'minimalist framework', meaning that Express.js handles a few core tasks well, but does not include many nice-to-have features. Instead, you can enhance your Express.js application with middleware downloaded from npm or that you build yourself.

This series has four parts.

First, we'll look at HTTP routing, the problem that Express.js tries to solve.

Second, we'll look at a bare-bones setup and configuration of Express.js.

Third, we'll look at practical routing in Express.js.

Fourth, we'll enter the wonderful world of middleware, to see how Express.js makes it easy to add useful features to your app.

Let's go!

HTTP Communication – The Problem

Networking occurs when servers and clients talk to each other.

In computing, a client refers to a computer that makes requests. A server refers to a computer that replies with a response. The internet, through TCP/IP, is the communication channel that servers and clients use to talk to each other.

Programmers often refer to servers as the back-end and clients as the front-end of a user's online experience. These are just servers and clients!

HTTP is a set of guidelines and standards for how this networking can occur. We don't need to know the details. Just remember that below Express' simple API, our applications are using HTTP to wire everything together.

Request and Response

After a server is launched it sits there, listening for requests.

If a client sends a request, we set off a loop that's at the core of online networking:

  • A client initiates a request
  • The server listens and receives the request
  • The server processes the request, perhaps validating data or interfacing with a database to craft a response
  • The server sends a single response back to the client

This back-and-forth between client and server forms the request/response cycle. Every HTTP request follows this cycle.

      Client  ---- 'the internet' : TCP/IP ----- server
      makes            connects through           sends
      request ===>        routing...       ====> response
      response <=====================================|

Note that for any requests there is one and only one response. When you visit a modern web page, you'll likely make many requests at once even though the page seems to load only one time.

One response may return the HTML of the page, another might deliver an image or a video, and another might deliver a CSS file necessary for styling the web page.

After the request/response cycle is complete, the client's computer and browser handle the rendering and visualization of the response. For example, the client's browser may display images that were sent by the server.

In short, when you type a URL into your web browser, you are making an HTTP request to a server. The website, files, message, or whatever else you receive is that server's response.

What's Inside a Request or Response?

Since HTTP is a set protocol, requests and responses have a predictable format. As we'll soon see, Express.js maps onto this format to make it easy to handle a response or request.

An HTTP request contains...

  • Route – What's our URL? (i.e. http://www.google.com)
  • Verb – Is this a GET/POST/DELETE or other request? Sometimes called 'method'. More on this later.
  • Headers – Meta-data about the request.
  • Payload – The actual data sent by the request, like a name submitted into an HTML form.

An HTTP response contains...

  • Status – The HTTP 'status code' and standard message of the response i.e. 200 / OK! People are most familiar with the 404 code, which is likely what you'll get if you try to access an invalid URL!
  • Headers – Meta-data about the response.
  • Body – The data sent back to the client, such as the HTML of a webpage or a file.

If you're a little confused by all this, you're not alone.

Routing HTTP In Your Website Or App: A Big Headache

Handling responses and requests on your website or application is no small feat. Every page and file on your website must be routed to the user at the appropriate time.

To receive data from the user, your server will need to read the request's payload. You'll also need to make sure the proper verb is tied to the right route, so that a GET (when your client receives data) does not act like a POST (when your client sends data). You'll need to send back correct status codes and, of course, make sure any files or other data arrive at the proper time.

Each subpage may also have its own route. For example, www.yoursite.com might offer one response and www.yoursite.com/pictures might offer another.

Imagine how many routes you would need on a complex website if you had to route the whole site, file by file, page by page!

The beauty of Express.js is that it massively simplifies all this routing, leaving you to focus on the overall architecture of your app or website.

In the next part of this series, we'll install and configure Express.js for our app.


Installing Express.js

To install Express.js, you'll first initialize an npm project in a directory of your choice. Then, create a file where you'll build your app. I'm calling mine app.js.

Now, run npm install --save express from your favorite command line interface. This will install express along with its many dependencies.

You'll also need an up-to-date version of Node.js, since Express leverages node's HTTP to work its magic.

Once Express.js is installed, we're ready to start building our app.

Require and Instantiate An Express App

First require Express. This makes the Express module available in our app.js file.

Our call to require('express') returns a function that we can invoke to create a new express application instance.

        // Inside app.js file
        const express = require('express'); // makes Express available in your app.

Now, we can invoke express and make our Express application available under the variable name app.

        const app = express();
        // Creates an instance of express, which allows us to begin routing.

With Express properly imported and our app instantiated, we're ready to start our server!

        app.listen(3000); // Starts up our server on port 3000.

app.listen(port) starts up our server, listening on the port that we pass in.

        module.exports = app;
        // Allows other files to access our Express app.

Finally, let's make our app available to other files by putting our app into module.exports.

Congratulations, you have launched a web server! Now, let's get it to do something.


Routing in Express.js

Now that our app is set up and configured, let's see how Express.js simplifies our routing.

Our First Route

Here's what our app currently looks like.

        const express = require('express'); // makes Express available in your app.
        const app = express();  // Creates an instance of Express, which allows us to begin routing.

        app.listen(3000);  // Starts up our server on port 3000.
        module.exports = app;  // Allows other files to access our Express app.

Let's add our first route above where we start our server.

        const express = require('express'); // makes Express available in your app.
        const app = express();  // Creates an instance of express, which allows us to begin routing.

        // OUR NEW ROUTE
        app.get('/home', function(request, response, next) {
            response.send(200);
        });

        app.listen(3000);  // Starts up our server on port 3000.
        module.exports = app;  // Allows other files to access our Express app.

What's going on here?

If you recall from our the first article in this series, HTTP requests all have a verb like GET or POST. GET is probably the most common HTTP verb. It's used for when the client is trying to retrieve a file or page from the server.

When we create a route in Express, we first specify the verb that our HTTP request will use. The get in our app.get is our verb.

app.get is a function and takes the route we want use as its first argument. In this case, we are routing to '/home'.

That means that all GET requests to localhost:3000/home will be handled by this route.

Next we see a callback function:

      function(request, response, next) { });

This function is called when the user requests the route. Here's where all the magic happens! Let's look at this function more closely.

Request & Response

In Express.js, requests and responses are handled through three core objects, typically named request (request), response (response), and next. request and response are the two sides of HTTP communication. We'll talk about next in a moment.

The request object contains properties that allow us to access the components of our HTTP request.

For example:

  • We can call request.body to see the payload of the HTTP request
  • We could call request.method to see if the request was a GET, POST, or other HTTP verb.
  • We could call request.ip to see the IP address that's requesting our site.

Check out the Express documentation for how to access the many properties of your request object!

Remember, every HTTP request needs one and only one response. Here's where our res object comes in.

An HTTP response might send the user a status code, some files, a website, or more!

With our response object, we have methods to easily craft a response.

In our example above we call response.send(200). This sends the 200 status code back to the user when they request our site. We can also call methods like response.render(template, data) to render an HTML page or response.redirect(anotherURL) to send users somewhere else.

As with request, just check out the docs for how to get the most out of your response object.

With these two objects in our callback, we have everything we need to complete the HTTP request/response loop.

Next

So what's up with next?

Next is a callback that we can invoke to tell Express to search for another route that satisfies our request.

Let's say that we don't want our webpage to be accessible by a particular IP address. We use an if block and our request object's .ip property to block that IP by sending it a 404 status code.

        app.get('/home', function(request, response, next) {
            if (request.ip ==== '192.168.1.123') {
              response.send(404);
            } else {
              next();
            }
        });

For every other ip, we call next(). When we call next(), Express will search our routes for the next handler that match the request. In this case, Express would search for a handler that manages a GET request to our '/home' route.

Let's add one.

        app.get('/home', function(request, response, next) {
            if (request.ip === '192.168.1.123') {
              response.send(404);
            } else {
              next(); // Looks for our next relevant handler
            }
        });

        // Here's another GET route to /home!
        app.get('/home', function(request, response, next) {
            response.render(myHomePage, myDataObject);
            // ^ Render my home page with my data (defined elsewhere)
        });

Next is great for keeping individual routes short and clear, while still permitting control flow through different routes called through next.

Simplify Your Life with Express Routers

So far we've handled our requests and responses with ease. But we still haven't seen how Express solves the annoying problem of routing the many resources and pages that a modern website may have.

Routing to Sub-Resources

Let's imagine that our website has a sub-page dedicated to a collection of kittens at '/kittens'.

Easy, right? As we saw before, we could just add a route to our app like this:

      app.get('/kittens', function(request, response, next) {
          response.render(myKittenPage, myCollectionOfKittens);
      });

But what happens if we want to view individual kittens? A user might want to click on a kitten photo and arrive at an individual page with data only about that kitten.

In our database, kittens could be assigned an ID. When a user clicked a picture they would reach a kitten's page, identified by id.

Let's do this the dumb way first:

      app.get('/kittens/1', function(request, response, next) {
          response.render(myKittenOnePage, dataForKittenOne);
      });

      app.get('/kittens/2', function(request, response, next) {
          response.render(myKittenTwoPage, dataForKittenTwo);
      });

      app.get('/kittens/3', function(request, response, next) {
          response.render(myKittenThreePage, dataForKittenThree);
      });

      ... et cetera ...

You get the idea. This manual routing, which Express won't stop you from doing, quickly becomes a chore.

Even if everything works fine, it makes our app harder to maintain and understand. All these routes clutter up our app.js file and mix our apps configuration and top-level logic with tons of routes for sub-pages. Bad idea!

It would be better if could make our code more modular. In one file, we can handle our configuration and top-level routes. In another, we handle the details of our routing.

Make a Router

Express gives us a Router object that makes handling routes like these a breeze. In essence, we're going to encapsulate our routes in the same way that a good programmer would encapsulate their code in clear, separate objects and files.

Express.js makes this remarkably easy with Express.Router().

Express.Router() returns a router object that we can attach routes to. Then, we .use() the router in our Express app and voilá, our routes are configured!

Instead of adding more routes to our app.js files, let's make a new file called kittens.js.

Inside kittens.js, we'll make almost everything we need.

    // inside kittens.js
    const express = require('express'); // Import Express
    const kittens = express.Router(); // Create a Router object named kittens


    // Our route to handle requests to /kitten
    kittens.get('/', function (request, response, next) {
      let myKittenData = {}
      /* Do any database/server logic to retrieve all of our kitten data here */

      response.render(myKittenPage, myKittenData)
    });

    // Our route to handle routes to an individual kitten's page
    kittens.get('/:id', function(request, response, next) {
      let kittenID = request.params.id; // get ID of kitten from the request URL

      let myKittenData = {}
      /* Use kitten ID to do any database/server logic to retrieve individual kitten data here */

      response.render(myKittenPageTemplate, myKittenData);
    });

    module.exports = kittens; // Make sure that Kittens is available to our other files, so we can use it in our app.js

Inside our kitten.get(), we retrieve the id of the kitten we want to view based on the URL that the user requested.

Remember, our request is just an object, which has a property called params (short for 'parameters'). This parameters object will contain properties based on the routed URL. In our GET, we specify :id as one of our request's parameters.

This means that if the user clicked on the twelfth kitten in our collection which linked to '/kittens/12', we would retrieve the kittenID '12' because the routed URL would be /kittens/:id where :id is 12.

Once we retrieve our ID, we would need to run any logic to retrieve kitten data from our database. That's beyond the scope of this article. Once we have our data, we would pass it in as an object myKittenData to fill out our HTML template, myKittenPageTemplate.

Params and Query Strings

In the last example, we called the params property from our request object.

Sometimes, routes include query strings. You can spot a query string because it follows a ? in the URL.

Let's imagine we want to filter our kittens by color. Users might select to see only black cats from a form, dropdown menu, or other input.

We'll end up with a query that looks something like this '/kittens?color=black'.

Just as our request had a .params property, it contains a query property, too! Express parses our URLs and allows us to access properties like these with request.query.

request.query will return an object if there is a query string. If there isn't, that object will be empty.

    // Our route to handle routes to an individual kitten's page
    kittens.get('/', function (request, response, next) {
      let myKittenData = {}
      let kittenFilter = request.query.color // gets a the 'color' query string if it exists

      if (Object.keys(kittenFilter).length) { // If we have a filter...
        /* Do any database/server logic to retrieve kittens by COLOR here */
        response.render(myKittenPage, myFilteredKittenData) // apply filter and respond with kittens

      } else {
        /* Do any database/server logic to retrieve all of our kitten data here */
        response.render(myKittenPage, myKittenData) // respond with all kittens
      }
    });

    module.exports = kittens; // Make sure that kittens is available to our other files, so we can use it in our app.js

Export Your Router

There's just one final step. Back in our app.js file, we need to use our new router. Let's add the following:

    // inside app.js
    const kittens = require('./kittens.js'); // specify the directory path to your router file here

    app.use('/kittens', kittens);

That's it!

Now, anytime a user enters a URL in the form of '/kittens', our router will take over. They'll be sent to our '/kittens' page (the '/', or root route, in our kittens router). Plus, if any user clicks on an individual kitten, they'll be sent to the appropriate page for that kitten's ID through our Router's '/kittens/:id' route.

We've gone from dozens or hundreds of routes to one, encapsulated router. Routers make linking sub-pages or resources much easier.

When we call .use() for our Kitten router, we take our first step into the world of Express middleware. Let's see some other ways to improve our app with .use().

Express.js – Middleware

Middleware refers to modules that are chained 'between' when your Express app is instantiated and when your server is launched with app.listen(myPortNumber).

We'll stick to two easy examples that illustrate the usefulness of middleware: logging and static resources.

Registering and Using Middleware

Using middleware is simple. Use node.js to require the package that you'd like to use. Then add the middleware to your app by calling it with .use(). The order that you call your middleware may matter, so look in the docs for your middleware to make sure it's well-positioned.

Here's how we would deploy the logging middleware Morgan.

First, npm install morgan. We'll use the same express configuration that we did before.

        const express = require('express'); // makes Express available in your app.
        const app = express();  // Creates an instance of Express, which allows us to begin routing.
        const morgan = require('morgan'); // Require our middleware


        app.use(morgan('dev')); // use our middleware. Check docs for the mode ('dev') you'd like to use.


        app.listen(3000);  // Starts up our server on port 3000.
        module.exports = app;  // Allows other files to access our Express app.

That's it!

Static Resources

One extremely useful bit of middleware is Express' static resource routing method.

As we saw before, all of your assets must be routed properly in your application. Without static resource routing, commonly-used assets like a logo must be routed relative to every page that uses them. What a nightmare!

Thankfully, Express allows us to set a folder as our 'static' directory. This directory, usually called 'public', holds commonly-shared files like a logo image. Here's how we would .use() static routing.

      const express = require('express');
      const app = express();
      const morgan = require('morgan');

      app.use(morgan('dev'));

      app.use('/static', express.static(__dirname + '/public')); // Sets up static resource routing to our /public directory

      app.listen(3000);
      module.exports = app;

Once configured, your HTML can reference any file in your 'public' directory by putting '/static' in front of the route. A logo could be referenced as '/static/kittensLogo.png' on every page your website. No more painful manual routing for your shared files.

You might notice that our call to .use() looks similar to any other route. It is. When our application makes a request to a file at '/static', our express.static router takes over and serves up the file from our public directory. This means that the path '/static' is just a convention. Since .use() operates just like any other route, so you can specify whatever you want.

Body Parsing

If you call, our HTTP request object sometimes contains a body (payload) of information from the user. For various reasons that are beyond the scope of this article, parsing the data from this body can be annoying.

Thankfully, Body Parser exists. Body Parser can be configured to automatically parse JSON and URL encoded strings in HTTP. As with other middleware, set up is simple.

First npm install body-parser. Then, using our original Express configuration:

      const express = require('express');
      const app = express();
      const morgan = require('morgan');
      const bodyParser = require('body-parser'); // Require body-parser

      app.use(morgan('dev'));
      app.use(express.static(__dirname + '/public'));

      // BODY PARSER
      app.use(bodyParser.urlEncoded()); // Tells Express to use body-parser to parse url encoded strings
      app.use(bodyParser.json()); // Tells Express to use body-parser to parse JSON


      app.listen(3000);
      module.exports = app;

Once you have Body Parser configured, you'll be able to access the .body property on your request object. The body is itself an object, which contains whatever came through the request. For example, if a user were to send form data for a field title 'username', we could access their input by using request.body.username. You can then use the body to do whatever you want in your route.

      // Our route to handle requests to /kitten
      kittens.get('/', function (request, response, next) {
        let myBody = request.body;
        response.render(myKittenPage, myBody)
      });

Error-Handling Middleware

Since your Express app relies on asynchronous functionality, you will need to handle your errors. Errors are typically handled with a .catch() at the end of a promise chain.

Here's how that would look:

app.get('/path', function (req, res) {
  AsyncFunction()
  .then(function(resultsOfAsync) {
    // do stuff
  })
  .then(function(moreResultsOfAsync) {
    // do more stuff
  })
  .catch(console.error) // If an error is thrown, our catch block will process it and error log it in the console.
})

This approach will work fine. But as your program grows, you'll find yourself writing many catch blocks. What happens if you want to do something a bit more customized with your error? Perhaps you want to color it in your console, do additional processing, or have some fallback response. You'll need to re-create this code in every .catch() – not exactly a DRY approach.

Express allows us to centralize error handling by defining error-handling middleware.

First, we can head to our main app (the same file we used in previous examples). We'll define our single, error-handling middleware here.

      const express = require('express');
      const app = express();
      const morgan = require('morgan');
      const bodyParser = require('body-parser');

      app.use(morgan('dev'));
      app.use(express.static(__dirname + '/public'));

      app.use(bodyParser.urlEncoded());
      app.use(bodyParser.json());

      // ERROR-HANDLING MIDDLEWARE
      app.use(function(err, req, res, next) { // A function with these four parameters will always be treated as error-handling middleware by Express.
        console.error(chalk.red.underline.bold(err)); // turns our error red, underlines it, and bolds it
        res.send(404); // sends a 404 to our user
      });

      app.listen(3000);
      module.exports = app;

We call app.use() so that Express will use our error handling on every route – if there's an error. We can define anything we want within our function(err, req, res, next). To create error-handling middleware, we just need to create a function with four parameters inside our app.use. Express will always see this as error-handling. Our first parameter will be our error.

There's two big benefits to this approach. First, we can do so much within our error handler, since we have access to req, res, and next.

Second, we can rewrite all of our .catch() blocks in an extremely simple way:

app.get('/path', function (req, res) {
  AsyncFunction()
  .then(function(resultsOfAsync) {
    // do stuff
  })
  .then(function(moreResultsOfAsync) {
    // do more stuff
  })
  .catch(next) // If an error is thrown, send it to our Error-Handling Middleware!
})

Now, we can use .catch(next) in every Promise chain. Our error-handing middleware will catch the error and do whatever we defined to handle it.

That wraps up our exploration of Express.js. Happy building!

@DavidKelleySCSC
Copy link

Succinct and to the point. One well written article to bring it all into focus for me. Many thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment