Skip to content

Set up Routes for Your API in Node.js

Routing directs an incoming HTTP request to the appropriate action in our Node.js API. In this tutorial we will create a routes module in which we’ll define the endpoints for the Node.js Express server. We will also create handlers for our routes which will use the functions we created earlier to make requests to the service and return a transformed response from our API.

By the end of this tutorial, you should be able to:

  • Understand how to create modular Express routes in Node.js
  • Explain what an Node.js Express router instance is

This tutorial is part 4 of 7 tutorials that walk through using Express.js to create an API proxy server.

Goal

Create the routes for the API using the Node.js Express framework.

Prerequisites

Watch: Set up Routes for Your API

Overview

In this tutorial we are going to set up the routes for our API Proxy using an Express router instance. Using a router allows us to approach building an API server in a modular way, separating different functions of the application, and letting us organize related routes and middleware into a single file.

We will create route handlers to retrieve data from the Exoplanet API, transform the planet data we receive, and send it back to the client. We will also create a middleware that only runs for the routes attached to the specific router instance we are creating.

Create a routes directory and a planets.js to hold our routes:

Terminal window
mkdir routes
touch routes/planets.js

Open the routes/planets.js file in your editor and create a router instance, which we will use to register the routes for the API. Then export the router instance from the file:

const express = require("express");
const router = express.Router();
module.exports = router;

We’ve now created an instance of an Express router. We export the router instance from the file so we can later require it in our main server file (index.js) and register the routes with the Express application.

Let’s stub out the routes that we are going to be creating:

  • /planets to retrieve all planet records from the Exoplanet API
  • /planets/since/:year to retrieve all planet records discovered since a supplied year

We will be mounting the router at the path /planets with the application, so the routes we define can leave off the /planets part that they all share. Any path our router defines will have /planets appended to the path after we mount the router.

router.get("/", (req, res) => {
res.send("planets!");
});
router.get("/since/:year", (req, res) => {
res.send(`planets since ${req.params.year}`);
});

The second route /since/:year takes a parameter as the last part of the path, which is available on the request object at req.params.year. We will use this supplied parameter when requesting planets from the Exoplanet API.

With these stubbed out routes, we can go ahead and register the router instance with the Express app itself by requiring the routes module in our main index.js file and passing it to app.use():

const express = require("express");
const planetRoutes = require("./routes/planets");
const PORT = 3000;
const app = express();
app.use("/planets", planetRoutes);
app.listen(PORT, () => console.log(`Server is listening on port ${PORT}`));

We mounted the router at the path /planets. This ensures that the middleware the router uses will only be run for /planets routes, as well as appending /planets to all the paths the router has defined.

Start the server and then visit localhost:3000/planets and you should see planets! in the browser window:

Terminal window
npm run dev

Now let’s implement our actual routes back in the routes/planets.js file. First we will need to require the request functions and transform function from the module we created earlier for the Exoplanet API:

const {
getAllPlanets,
getPlanetsDiscoveredSinceYear,
transformOnePlanet
} = require("../exoplanet");

Then create a route handler at the root of the /planets route to retrieve the data from the Exoplanet API, transform the response, and send it back to the client as a JSON payload. We are going to use async/await, so we also need to prefix the route handler with the async keyword.

router.get("/", async (req, res) => {
const rawPlanets = await getAllPlanets();
const transformedPlanets = rawPlanets.map(transformOnePlanet);
res.json(transformedPlanets);
});

We use getAllPlanets to make a request to the Exoplanet API (the API we are proxying requests to) to retrieve the planet records. We want to transform the raw data from the Exoplanet API before sending it back to the client, so we map over each record and apply transformOnePlanet to create a new array of transformed planet records. Finally, once the data has been retrieve and transformed, we send it back to the client as a JSON payload using res.json.

Visit localhost:3000/planets in your browser and you should see all the transformed planet records returned as JSON. It may take a few seconds to load, as the Exoplanet API can be slow.

An important step that we skipped, though, is handling errors that might occur. Express route handlers can take a third argument, next, which can be passed any errors that occur when handling a request. When doing async work (either with promises, callbacks, or async/await) it is essential to handle errors and pass them to next. If you don’t your application may hang, crash, or otherwise end up in an unpredictable state.

To improve our route handler we need to do two things:

  • Add next to the route handlers arguments
  • wrap the await in a try/catch block to catch any thrown errors

Then we’ll pass any errors that are caught to next in the catch block:

router.get("/", async (req, res, next) => {
try {
const rawPlanets = await getAllPlanets();
const transformedPlanets = rawPlanets.map(transformOnePlanet);
res.json(transformedPlanets);
} catch (err) {
next(err);
}
});

We’ll do the same for the /since/:year route. Make sure to add the next argument to the route handler as well.

For this handler we are also using the :year parameter which is part of the path for this route, passing it into the getPlanetsDiscoveredSince function:

router.get("/since/:year", async (req, res, next) => {
try {
const { year } = req.params;
const rawPlanets = await getPlanetsDiscoveredSinceYear(year);
const transformedPlanets = rawPlanets.map(transformOnePlanet);
res.json(transformedPlanets);
} catch (err) {
next(err);
}
});

Visit localhost:3000/planets/since/2020 to see all planets discovered since 2020.

One of the benefits of using router instances to organize your routes is encapsulating middleware to a specific router instance. You can apply router level middleware which will only run for the routes at the path the router instance it is attached to.

You might have noticed the Exoplanet API can be slow to respond. Let’s add a middleware to the router instance that will tell us how long it took to send a response back to the client for our planets routes.

To do that we will create a middleware function that attaches a listener to the res object’s finish event. The finish event is emitted once the response has been sent to the client. This lets us compare a start time to an end time and figure out how many seconds elapsed. We’re creating this middleware manually as an example, but in a production-ready application you could use an HTTP logger package like Morgan to do this for you.

Add this code directly after creating the router instance, but before you declare the route handlers:

// should be after this line
// const router = express.Router()
router.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
const end = Date.now();
const diffSeconds = (end - start) / 1000;
console.log(`${req.method} ${req.url} Completed in ${diffSeconds} seconds`);
});
next();
});
// and before your router.get lines of code

If your server never responds after you add this code, make sure you are calling next outside of the event listener!

Open your browser to localhost:3000/planets/since/2020 and you should see something like this in your terminal once the request completes:

Terminal window
GET /planets/since/2020 Completed in 0.688 seconds

Visit localhost:3000/planets and you should see that this request takes much longer! It can take 30 seconds or more to complete, but will be variable depending on how long the Exoplanet API took to respond:

Terminal window
GET /planets Completed in 29.436 seconds

This is a simple example of applying a middleware on a router level. But you can use the same approach if you wanted to add Authorization middleware, caching, or implement any other middleware functionality on only a particular collection of routes organized under the same router instance.

If you had registered this middleware with the main app instance of Express, it would apply to every route that is registered after the middleware. Using router instances lets you take a more modular approach to applying middleware to your routes than defining routes directly on an app instance of Express.

Recap

In this tutorial we created a router instance to hold the routes for retrieving data from the Exoplanet API. We defined route handlers that access the Exoplanet API and transform the response before sending it back to the client. When performing async work like making an API request in a route handler, it’s important to make sure you catch any thrown errors and pass them to next so Express can handle the errors. We also created a route level middleware to see how long our route handlers took to complete.

Keep going with the next tutorial in this set: Optimize an Express Server in Node.js.

Further your understanding

  • How is a router instance different from registering routes directly on an Express app instance?
  • When would you want to register a middleware at the app level instead of the router level? What about at an individual route handler level?
  • If the getAllPlanets function was callback-based instead of Promise-based, how would dealing with errors and passing them to next be different?

Additional resources