Express.js Router
Learn how to define and use routes in Express.js applications, including CRUD methods, dynamic routes, wildcards, how to organize routes, use globals, handle errors, etc.
If you’re completely unfamiliar with Express.js, I recommend reading the introduction chapter first.
Route Handler
Every endpoint (route) has a handler method that looks like this:
router.METHOD(path, [callback, ...] callback)
Let’s break it down.
1. The router
is an instance of the Express.js (typically called app
)
const express = require('express');
const app = express();
app.METHOD(path, [callback, ...] callback)
2. The method
represents a set of HTTP methods (verbs), such as:
- GET, POST, PUT, PATCH, DELETE, etc.
app.get(path, [callback, ...] callback)
3. The next part is the route URI, typically referred to as the path
. When this path is visited, the endpoint is triggered, and the callback function is executed. Every path is like a unique address to a location.
Here are a few examples:
app.get('/', [callback, ...] callback)
app.post('/login', [callback, ...] callback)
app.delete('/api/user/:id', [callback, ...] callback)
This is how you invoke these paths:
// GET
> curl http://localhost:3000/
// POST
> curl -X POST http://localhost:3000/login
// DELETE
> curl -X DELETE http://localhost:3000/api/user/123
4. The final part is the callback function containing two objects the request
and the response
. As stated before, this function is triggered after visiting the route path:
app.get('/', (req, res) => {
// do stuff
});
app.get('/', (req, res) => {
res.send('Hello World');
});
Shared URI
Express.js lets you define as many routes as you desire, giving each a unique response. However, If multiple routes share the same route URI, only the first route will be called.
Multiple URIs
A route handler can also have multiple URIs (as shown in the array):
app.get(['/', '/home'], (req, res) => {
res.send('Welcome home!');
});
Visiting either route
will produce the response (‘Welcome home!’
).
Dynamic URI Parameters
A route URI can be dynamic as well. This is typically used when retrieving items by ID from the database.
The dynamic input parameters are read via req.params
object, (e.g. req.params.id
) where id
is the name of the parameter in the route:
app.get('/:id', (req, res) => {
const id = req.params.id;
res.send(id);
});
> curl http://localhost:3000/123
=> 123
> curl http://localhost:3000/jose
=> jose
Optional parameters
Dynamic parameters can be optional as well. To achieve this, add a question mark after the dynamic parameter (:id?
):
app.get('/:id?', (req, res) => {
const id = req.params.id;
if (id) {
res.send(`Received id: ${id}`)
} else {
res.send('Id was not received. Defaulting to id: 1')
}
});
> curl http://localhost:3000/123
=> 'Received id: 123'
> curl http://localhost:3000/
=> 'Id was not received. Defaulting to id: 1'
Regular Expression Pattern URI
Express.js route paths also support regular expressions to define routes that match specific patterns.
app.get(/pattern/, callback);
An example of this could be a route that matched any path starting with the /api
path:
app.get(/^\/api/, (req, res) => {
res.send('Hello from API!');
});
> curl http://localhost:3000/api
=> Hello from API!
> curl http://localhost:3000/api2hello
=> Hello from API!
> curl http://localhost:3000/api/users
=> Hello from API!
The RegEx patterns can also be used with dynamic route parameters. Here is a route that requires a product ID, where the ID must be a digit.
app.get('/api/product/:id(\\d+)', (req, res) => {
res.send(`Product ID: ${req.params.id}`);
});
> curl http://localhost:3000/api/product/500
=> Product ID: 500
If you pass anything other than a number, the response code will be 404:
> curl http://localhost:3000/api/product/hello
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /api/product/hello</pre>
</body>
</html>
Wildcards
The wildcards are used to capture multiple routes that match a certain pattern. The wildcard is the asterisk symbol *
. In this example, the route matches anything that starts with /api
(just like with RegEx):
app.get('/api*', (req, res) => {
res.send('Hello from API!');
});
> curl http://localhost:3000/api
=> Hello from API!
> curl http://localhost:3000/api2
=> Hello from API!
> curl http://localhost:3000/api/users
=> Hello from API!
The wildcards are typically used for error handling as the *
route will capture all route paths:
app.get('*', (req, res) => {
res.status(404).send('Sorry Mario. Our princess is in another castle!');
});
For example, I created three main routes:
- Home page
- About page
- Profile page
And a wildcard route.
app.get('/', (req, res) => {
res.send('Welcome home!');
});
app.get('/about', (req, res) => {
res.send('About page!');
});
app.get('/profile', (req, res) => {
res.send('Profile page!');
});
// Catch-all route
app.get('*', (req, res) => {
res.status(404).send('Page not found!');
});
The wildcard route that will match any route besides these three:
> curl http://localhost:3000
Welcome home!
> curl http://localhost:3000/about
About page!
> curl http://localhost:3000/profile
Profile page!
> curl http://localhost:3000/anywhereelse
Page not found!
It’s important to put the catch-all wildcard route after all defined routes. If you change the order of routes, Express.js will ignore all the subsequent routes and redirect to the catch-all route.
app.get('/', (req, res) => {
res.send('Welcome home!');
});
// Catch-all route
app.get('*', (req, res) => {
res.status(404).send('Page not found!');
});
app.get('/about', (req, res) => {
res.send('About page!');
});
app.get('/profile', (req, res) => {
res.send('Profile page!');
});
> curl http://localhost:3000/
Welcome home!
> curl http://localhost:3000/about
Page not found!
> curl http://localhost:3000/profile
Page not found!
Express.js also lets you define global variables that can reused through the app. Use app.set()
to set the global variable.
app.set('greetings', { data: 'Hello World!' });
And app.get()
to retrieve it:
app.set('greetings', { data: 'Hello World!' });
app.get('/', (req, res) => {
const greetings = app.get('greetings');
res.send(greetings); // Hello World!
});
This is useful for preserving environment variables throughout the app or setting the templating engine (for server-side rendering template views).
Create Read Update Delete Routes
Here is how Express.js implements different CRUD methods:
- GET — used to retrieve resources from the server
app.get('/', (req,res) => {
res.send('Get Foo');
});
- POST — used to create a new resource
app.post('/', (req,res) => {
res.send('Create Foo');
});
- PUT / PATCH — used to update a resource
app.put('/', (req,res) => {
res.send('Update Foo');
});
app.patch('/', (req,res) => {
res.send('Update Foo');
});
- DELETE — used to delete a resource
app.delete('/', (req,res) => {
res.send('Delete Foo');
});
All Route
The app.all()
method matches all HTTP methods (verbs).
app.all('/', (req, res) => {
const requestMethod = req.method.toUpperCase();
if (requestMethod === 'POST') {
res.status(201).send('Entity created!');
} else if (requestMethod === 'GET') {
res.send('Entity retrieved!');
} else {
res.status(404).send('Method not supported!');
}
});
> curl http://localhost:3000/
=> Entity retrieved!
> curl -X POST http://localhost:3000/
=> Entity created!
> curl -X PUT http://localhost:3000/
=> Method not supported!
The typical use case is the global request interceptor or an API gateway. Another example is the catch-all wildcard route to capture non-existent routes for any HTTP method:
app.all('*', (req, res) => {
res.status(404).send('Method not supported!');
});
Tips to better organize route handlers.
Chaining Router actions
If you want to keep all methods on the same line, you can chain them together without using the semicolon:
app
.get('/', (req, res) => {...})
.post('/', (req, res) => {...})
.put('/', (req, res) => {...})
.delete('/', (req, res) => {...});
Separating Routers
A common practice is to separate routes per feature. Suppose you have two features:
- Users,
- Stories
You can move all user-related routes in the Users router and likewise for Stories routes. Here is how you can do that:
- Create a new javascript file (e.g. story.router.js)
- Move all “Story” related endpoints inside:
// story.router.js
const router = require('express').Router();
router
.get('/', (req, res,) => {
...
})
.get('/:id', (req, res) => {
...
})
.post('/', (req, res) => {
...
})
module.exports = router;
- Export the router (
module.exports = router
) to be globally accessible - Import the Story router in the app.js file:
// app.js
const app = express();
app
// Parse JSON
.use(express.json())
// Add story routes
.use('/api/story', require('./story.router'))
.listen(3000, () => {console.log('Server started on port 3000');})
The URI (/api/story
) will prefix every router from the story router.
> curl http://localhost:3000/api/story
=> retrieves a list of story objects
It also makes the main file (app.js) much cleaner.
Alternate App Initializer
There is an alternative (less popular) way to initialize the Express.js app.
Standard:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server started on port 3000!');
});
Alternative:
const app = require('express')();
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server started on port 3000!');
});
Although the second approach is shorter and lacks a temporary variable, it is not as modular as the first. This is noticeable when working with middleware functions or writing tests. In apps, you’ll see the app object exported so it becomes publicly accessible in the unit tests:
const express = require('express');
const app = express();
/* stuff */
module.exports = app;
The second approach doesn’t separate the app
creation from its configuration, making it harder to use the app
instance in tests. Most examples, libraries, and tutorials use the standard approach.
Default Error Handler
Express.js comes with a basic default error handler. In case of an unhandled exception, Express.js will return the error to the user, but the server will be up and running.
You can test this by creating the successful and failed routes:
app.get('/', (req, res) => {
res.send('Hello World!');
})
app.get('/error', (req, res) => {
throw new Error("Gotta Catch 'Em All!");
})
When visiting the /error
route, the server will return an exception (as expected), but if the client then visits the successful route, the response will be 200 (as if the error never occurred).
- First request (error)
> curl http://localhost:3000/error
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Internal Server Error</pre>
</body>
</html>
- Second request (success)
> curl http://localhost:3000/
Hello World!
As shown, Express.js gracefully handles errors (on its own) without shutting down the server.
Default error messages
The error response message varies depending on the environment where the app is running. This is what the error looks like in development mode:
The response is a stack trace with status code 500 (Internal server error).
To switch to the production mode, set the default Node.js environment variable (NODE_ENV) to production. This can be done when starting the app in the package.json file.
Since I’m using Windows, I need the cross-env package to set this up.
> npm i cross-env
Upon finishing the installation, prefix the “start” script in the package.json file cross-env NODE_ENV=production
, like this:
"scripts": {
"start": "cross-env NODE_ENV=production node app.js"
},
Now rerun the app (npm start
) and you should see the production mode set in the console:
> cross-env NODE_ENV=production node app.js
Server started on port 3000!
Visiting the same error URL should display just dispay the text Internal Server Error
with again status code 500.
Error Handling Callback-Based APIs
This pattern is the default way of handling Asynchronous Exceptions in Node.js/Express.
When performing an async operation, like reading or writing a file or subscribing to an event, the outcome of the operation results in the callback function. The first parameter is always an error that occurred, and the second is the successful data response:
fs.writeFile((error, data) => {
if (error) {
// Error path
}
console.log(data) // Success path
})
This pattern is also used when handling errors within a middleware.
Error Handling Promise-Based APIs
Error handling with Promises is pretty straightforward. Promise class exposes the catch()
method used for handling errors produced by the Promise.
new Promise()
.then(data => data.json()) // Success path
.catch(error => console.log(error)) // Error path
It’s common practice to use Async Await
when working with promise-based APIs. This time around, the error handling is done using the Try-Catch
block:
app.get('/', async (req, res) => {
try {
const users = await User.find({})
res.send(users);
} catch (ex) {
res.status(400).send({ message: ex.message })
}
});
- In the
try
block, you await the resource (like an item from the database) and send it back to the client. - In the
catch
block, you read the exception message and send it to the client with an invalid status code (in this case, 400 — bad request).
A better approach is to handle all errors in place using the error-handling middleware—more on this in the upcoming Middleware functions blog.
Wrapping up
If you made it this far, give yourself a pat on the back. Making the most of the Express.js router will make your backend code cleaner and less error-prone.