Express Js Routing - a closer look at router techniques

01 Apr 2019

Welcome to express routing! In the installing express js example you used the first routing technique. The first routing technique is the app.method functions.

Express provides functions for most HTTP verbs like get, post, put, delete, head, and patch. To see the complete list visit this section of the express documentation.

Express Route Methods

// POST requests typically create new resources if you are following
// RESTful design practices.
app.post('/user', (req, res) => {
  res.send('POST - create a new user in your application');
});

Now that the user is created there should be a route to fetch their details.

// GET requests are typically READ ONLY. This means running them does
// not change application state or database state.
app.get('/user/:userId', (req, res) => {
  // Express parsers what comes after /user/ into a variable on the
  // params object in the request.
  const { userId } = req.params;
  // example user lookup by userId
  const user = fetchUserFromDatabase(userId);
  res.send(user);
});

You can use regular expressions to define route paths. I think regex defined routes are useful and they are a more advanced feature.

app.get(/user_name\/(will.*)/, (req, res) => {
  const userName = req.params[0];
  res.send(`Matches usernames that begin with will ${userName}`);
});

If you do a GET request for /user_name/angela the server will reply with a 404.

If you do a GET request for /user_name/will-laurance the server will match to the route handler defined above.

I think Regex paths are advanced because they come with the dangers and complexity of regular expressions. Checkout this article if you'd like to learn more about regex performance.

In each project I've worked on big and small I've never had to use a regex path. My general thoughts are that all of the complexity you are attempting to solve with regex matching could be broken down into a declarative style.

Express Router

You maybe wondering defining routes use app.method is great but how do I organize my routes? Will my application have tens or hundreds of routes?

If you are building an app that's under agile development sometimes code organization gets de-prioritized. Sometimes this happens over and over again. Before you know it, things are Very messy in your express application.

With Express Router, I want to show you my favorite way to organize express applications.

All of the examples so far you are using the built-in Router instance. But a beautiful thing about express is the Router class is public and it is mountable in your current app. Mountable means bolted-on or attached.

I like to think about Routers as namespaces. For example you can build a User router that is responsible for everything under /user.

Let's create simple user router that mock create, reads, updates, and deletes users.

const { Router } = require('express');

// https://expressjs.com/en/4x/api.html#router
const router = new Router();

// A very simple in memory array to act as our
// user data store.
const users = [
  {
    id: 1,
    name: 'You!'
  },
  {
    id: 2,
    name: 'Me!'
  }
];

// The index router for users. This is the handler that
// is called for GET /users.
router.get('/', (req, res) => {
  res.send({
    users
  });
});

// findUser is a middleware function that you can use to lookup
// a user in our in memory data structure (array). If the user
// is found by the id in the params, attach the user to the
// request object. If the user is not found, send a 404 response.
// Note that handlers further in the chain are only called if the
// next function is called. In the case of replying 404, you never
// call the next function.
const findUser = (req, res, next) => {
  const { id } = req.params;
  const user = users.find(u => String(u.id) === String(id));
  if (user) {
    req.user = user;
    next();
  } else {
    res
      .status(404)
      .send('User Not Found');
  }
};

// the GET /users/1 request will find this handler. This route uses
// the findUser middle function defined above. The final function in the
// chain is very simple. If the last function is called we know we have a
// valid user attached to the request object.
router.get('/:id', findUser, (req, res) => {
  res.send({
    user: req.user,
  });
});

// the POST /users function creates a new user in our data structure.
// The API contract is that the POST body (json or multipart) is the
// user attributes. For this example the only required property is name.
// The only disallowed property is id. I love how we can use object destructuring to
// override any potential id passed in via body. It's a subtle thing, that's
// why I like to leave comments in areas with some "cleverness" to them.
router.post('/', (req, res) => {
  const { body } = req;
  // All users are required to have a name.
  if (!body.name) {
    return res.status(400).send('User must have name');
  }
  const user = {
    ...body,
    // handle ids internally
    // Find the largest id so far and add one.
    id: users.reduce((maxId, { id }) => {
      if (id > maxId) {
        return id;
      }
      return maxId;
    }, -1) + 1,
  };
  users.push(user);
  res.send({ user });
});

// PATCH /users/2 will find user 2 in the findUser middleware. If no
// user, then reply 404. Otherwise with the found user we are able to
// update every attribute about the user except the id. 
// Once we have the updated user object we can splice this back into the
// same array position as before.
// Finally we send back to modified user. This is good practice because
// some applications like to only trust the server response.
router.patch('/:id', findUser, (req, res) => {
  const { user, body } = req;
  const updated = {
    ...user,
    ...body,
    // cannot override user id
    id: user.id,
  };

  const index = users.map(u => u.id).indexOf(user.id);
  users.splice(index, 1, updated);

  res.send({
    user: updated
  });
});

// In this case findUser is useful because it handles
// sending a 404 if the user by id isn't found. If this were
// querying a database I wouldn't use this middleware. Instead I'd send
// a delete statement to the database (postgres, mongo, etc) and look for a success signal.
// No need to send back any information about the deleted user.
router.delete('/:id', findUser, (req, res) => {
  const { user } = req;

  const index = users.map(u => u.id).indexOf(user.id);
  users.splice(index, 1);

  res.send();
});

module.exports = router;

If this file was called ./userRouter.js you can incorporate it into your application index file like this:

// Require application dependencies
const express = require('express');
const { urlencoded, json } = express;

// Create our app by calling the express function
const app = express();

// Add in our middleware for ALL routes
app.use(urlencoded({
  extended: true,
}));
app.use(json());

// Register a route handler for GET requests made to /hello
app.get('/hello', (req, res) => {
  res.send('hello world');
});

// Register /users middleware
app.use('/users', require('./userRouter'));

// Get port from environment or default to port 3000.
const port = process.env.PORT || 3000;

// Ask our app to listen on the calculated port.
app.listen(port, () => {
  console.log(`Successfully express js server listening on port ${port}`);
});

Running your Express Router example

Run your updated file with:

node index.js

Now you can issue HTTP requests to your app:

# gets all users
curl http://localhost:3000/users

# delete user 1
curl -X DELETE http://localhost:3000/users/1

# create a new user
curl -X POST http://localhost:3000/users -d name="Your Name" -d age=25 -d location=earth

# update new users location
# You may need to adjust the id
curl -X PATCH http://localhost:3000/users/3 -d location=moon

Express Router Organization

Are you seeing how building custom router objects is cleaner than having hundreds of routes in your index.js file? In projects I've worked on I build out router files for most top level entities in my projects. For example in an e-commerce project, I'll have customer (or user), carts, orders, products, discounts, and coupons. I can build routers for each of these entities.

This simple user example does not get into the power of middleware. In an e-commerce project I don't want every API user to be able to create, update, or delete a coupon or discount. So I'll use authentication middleware to check to see if the current user is authorized to make that API call.

If you are interested, please follow the next post about express middleware.