Module 2 - Server-Side Routing
How does Routing Work?
Let's look at a basic example of routing in action.
First, to make our Express application respond to GET requests on different URLs, add the following endpoints:
// this request handler executes when making a GET request to /about
server.get('/about', (req, res) => {
res.status(200).send('About Us
');
});
// this request handler executes when making a GET request to /contact
server.get('/contact', (req, res) => {
res.status(200).send('Contact Form
');
});
Two things to note:
- We use the same HTTP Method on both endpoints, but Express looks at the URL and executes the corresponding request handler.
- We can return a string with valid HTML!
Open a browser and navigate to the /about and /contact routes. The appropriate route handler will execute.
How to Build It
Please follow along as we write endpoints that execute different request handlers on the same URL by changing the HTTP method.
Let's start by adding the following code after the GET endpoint to /hobbits:
// this request handler executes when making a POST request to /hobbits
server.post('/hobbits', (req, res) => {
res.status(201).json({ url: '/hobbits', operation: 'POST' });
});
Note that we return HTTP status code 201 (created) for successful POST operations.
Next, we need to add an endpoint for PUT requests to the same URL.
// this request handler executes when making a PUT request to /hobbits
server.put('/hobbits', (req, res) => {
res.status(200).json({ url: '/hobbits', operation: 'PUT' });
});
For successful PUT operations, we use HTTP Status Code 200 (OK).
Finally, let's write an endpoint to handle DELETE requests.
// this request handler executes when making a DELETE request to /hobbits
server.delete('/hobbits', (req, res) => {
res.status(204);
});
We are returning HTTP Status Code 204 (No Content). However, suppose you return any data to the client, perhaps the removed resource, on successful deletes. In that case, you'd change that to be 200 instead.
You may have noticed that we are not reading any data from the request, as that is something we'll learn later in the module. We are about to learn how to use a tool called Postman to test our POST, PUT, and DELETE endpoints.
Reading Data from a Request
Let's revisit our DELETE endpoint.
server.delete('/hobbits', (req, res) => {
res.status(204);
});
How does the client let the API know which hobbit should be deleted or updated? One way, the one we'll use, is through route parameters. Let's add support for route parameters to our DELETE endpoint.
We define route parameters by adding it to the URL with a colon (:) in front of it. Express adds it to the .params property part of the request object. Let's see it in action:
server.delete('/hobbits/:id', (req, res) => {
const id = req.params.id;
// or we could destructure it like so: const { id } = req.params;
res.status(200).json({
url: `/hobbits/${id}`,
operation: `DELETE for hobbit with id ${id}`,
});
});
This route handler will execute every DELETE for a URL that begins with /hobbits/ followed by any value. So, DELETE requests to /hobbits/123 and /hobbits/frodo will both trigger this request handler. The value passed after /hobbits/ will end up as the id property on req.params.
The value for a route parameter will always be string, even if the value passed is numeric. When hitting /hobbits/123 in our example, the type of req.params.id will be string.
Express routing has support for multiple route parameters. For example, defining a route URL that reads /hobbits/:id/friends/:friendId, will add properties for id and friendId to req.params.
Using the Query String
The query string is another strategy using the URL to pass information from clients to the server. The query string is structured as a set of key/value pairs. Each pair takes the form of key=value, and pairs are separated by an &. To mark the beginning of the query string, we add ? and the end of the URL, followed by the set of key/value pairs.
An example of a query string would be: https://www.google.com/search?q=lambda&tbo=1. The query string portion is ?q=lambda&tbo=1 and the key/value pairs are q=lambda and tbo=1.
Let's add sorting capabilities to our API. We'll provide a way for clients to hit our /hobbits and pass the field they want to use to sort the responses, and our API will sort the data by that field in ascending order.
Here's the new code for the GET /hobbits endpoint:
server.get('/hobbits', (req, res) => {
// query string parameters get added to req.query
const sortField = req.query.sortby || 'id';
const hobbits = [
{
id: 1,
name: 'Samwise Gamgee',
},
{
id: 2,
name: 'Frodo Baggins',
},
];
// apply the sorting
const response = hobbits.sort(
(a, b) => (a[sortField] < b[sortField] ? -1 : 1)
);
res.status(200).json(response);
});
Visit localhost:8000/hobbits?sortby=name, and the list should be sorted by name. Visit localhost:8000/hobbits?sortby=id, and the list should now be sorted by id. If no sortby parameter is provided, it should default to sorting by id.
To read values from the query string, we use the req.query object added by Express. There will be a key and a value in the req.query object for each key/value pair found in the query string.
The parameter's value will be of type array if more than one value is passed for the same key and string when only one value is passed. For example, in the query string ?id=123, req.query.id will be a string, but for ?id=123&id=234, it will be an array.
Another gotcha is that the names of query string parameters are case sensitive, sortby and sortBy are two different parameters.
The rest of the code sorts the array before sending it back to the client.
Reading Data from the Request Body
We begin by taking another look at the POST /hobbits endpoint. We need to read the hobbit's information to add it to the hobbits array. Let's do that next:
// add this code right after const server = express();
server.use(express.json());
let hobbits = [
{
id: 1,
name: 'Bilbo Baggins',
age: 111,
},
{
id: 2,
name: 'Frodo Baggins',
age: 33,
},
];
let nextId = 3;
// and modify the post endpoint like so:
server.post('/hobbits', (req, res) => {
const hobbit = req.body;
hobbit.id = nextId++;
hobbits.push(hobbit);
res.status(201).json(hobbits);
});
To make this work with the hobbits array, we first move it out of the get endpoint into the outer scope. Now we have access to it from all route handlers.
Then we define a variable for manual id generation. When using a database, this is not necessary as the database management system generates ids automatically.
To read data from the request body, we need to do two things:
- Add the line: server.use(express.json()); after the express application has been created.
- Read the data from the body property that Express adds to the request object. Express takes all the client's information from the body and makes it available as a nice JavaScript object.
Note that we are skipping data validation to keep this demo simple, but in a production application, you would validate before attempting to save it to the database.
Let's test it using Postman:
- Change the method to POST.
- Select the Body tab underneath the address bar.
- Click on the raw radio button.
- From the dropdown menu to the right of the binary radio button, select `JSON (application/json).
- Add the following JSON object as the body:
{
"name": "Samwise Gamgee",
"age": 30
}
Click on Send, and the API should return the list of hobbits, including Sam!
How to Build It
Please code along as we implement the PUT endpoint and a way for the client to specify the sort direction.
Implement Update Functionality
Let's continue practicing reading route parameters and information from the request body. Let's take a look at our existing PUT endpoint:
server.put('/hobbits', (req, res) => {
res.status(200).json({ url: '/hobbits', operation: 'PUT' });
});
We start by adding support for a route parameter the clients can use to specify the id of the hobbit they intend to update:
server.put('/hobbits/:id', (req, res) => {
res.status(200).json({ url: '/hobbits', operation: 'PUT' });
});
Next, we read the hobbit information from the request body using req.body and use it to update the existing hobbit.
server.put('/hobbits/:id', (req, res) => {
const hobbit = hobbits.find(h => h.id == req.params.id);
if (!hobbit) {
res.status(404).json({ message: 'Hobbit does not exist' });
} else {
// modify the existing hobbit
Object.assign(hobbit, req.body);
res.status(200).json(hobbit);
}
});
Concentrate on the code related to reading the id from the req.params object and reading the hobbit information from req.body. The rest of the code will change as this is a simple example using an in-memory array. Most production APIs will use a database.
The Basics of REST
REST (Representational State Transfer) is an architectural style for designing networked applications. RESTful APIs are designed around resources, which are any kind of object, data, or service that can be accessed by the client.
A RESTful API organizes things into resources that are accessible via URLs (endpoints) and uses HTTP methods to operate on these resources:
- GET - Retrieve a resource
- POST - Create a new resource
- PUT - Update an existing resource
- DELETE - Remove a resource
Key principles of REST include:
- Client-Server separation: The client and server are independent of each other
- Stateless: Each request contains all information needed to complete it
- Cacheable: Responses must define themselves as cacheable or not
- Uniform Interface: A standardized way to communicate between client and server
When building RESTful APIs, it's important to follow these conventions:
- Use nouns for resource names (e.g., /users instead of /getUsers)
- Use plural nouns for consistency (e.g., /posts instead of /post)
- Use HTTP methods appropriately
- Return appropriate status codes
- Format responses consistently
How to Build It
Let's explore the six key constraints of REST APIs in detail:
1. Client-Server Architecture
The client and server are separated and can evolve independently. This separation of concerns improves portability and scalability.
2. Stateless Architecture
Each request must contain all information needed to process it:
- Requests should stand on their own
- Order of requests should not matter
- No shared state between requests
3. Cacheable
Responses must explicitly state if they're cacheable. Caching:
- Improves network performance
- Stores data for faster future requests
- Eliminates repeated expensive operations
4. Layered System
The system can have multiple layers between client and server:
- Logging layers
- Caching layers
- DNS servers
- Load balancers
- Authentication layers
5. Code on Demand (Optional)
The API can return both resources and code to act on them:
- Clients only need to know how to execute the code
- Makes APIs more flexible and upgradeable
- Common in web apps sending JavaScript with data
6. Uniform Interface
This constraint has several key aspects:
- Each resource should have a single URL (recommended)
- Resource management through representations (URLs)
- Self-descriptive messages
- HATEOAS (Hypermedia As The Engine Of Application State)
Important note about HTTP methods:
- GET, PUT, and DELETE should be idempotent - multiple identical requests should have the same effect as a single request
- POST is not idempotent - each request may modify state differently
Routing in Express
An Express Router behaves like a mini Express application. It can have its own Routing and Middleware, but it needs to exist inside an Express application. Think of routers as a way of organizing Express applications–you write separate pieces that can later be composed together.
This should all become clear with an example.
We'll start with our main server file.
const express = require('express');
const server = express();
server.use('/', (req, res) => res.send('API up and running!'));
server.listen(8000, () => console.log('API running on port 8000'));
If our applications only included the above code, we wouldn't need routers. But imagine that this application needs endpoints to see a list of users, get details for a single user, add users, modify existing users, and inactivate users. That is at least five endpoints for the user's resource alone.
Now imagine this application also needs to deal with products, orders, returns, categories, providers, warehouses, clients, employees, and more. Even if we only have five endpoints per resource, each endpoint will have many lines of code, and you can see how trying to cram all that code in a single file could get unwieldy fast.
Let's rewrite it to separate the main server file from the file handling the routes for users.
Create a file to handle all routes related to the user resource.
// inside /users/userRoutes.js <- this can be place anywhere and called anything
const express = require('express');
const router = express.Router(); // notice the Uppercase R
// this file will only be used when the route begins with "/users"
// so we can remove that from the URLs, so "/users" becomes simply "/"
router.get('/', (req, res) => {
res.status(200).send('hello from the GET /users endpoint');
});
router.get('/:id', (req, res) => {
res.status(200).send('hello from the GET /users/:id endpoint');
});
router.post('/', (req, res) => {
res.status(200).send('hello from the POST /users endpoint');
});
// ... and any other endpoint related to the user's resource
// after the route has been fully configured, we then export it to be required where needed
module.exports = router; // standard convention dictates that this is the last line on the file
Even if the user resource needs 8 or 10 endpoints, they are packaged neatly into this file.
How can we use it in our main file? Like so:
const express = require('express');
const userRoutes = require('./users/userRoutes');
const productRoutes = require('./products/productRoutes');
const clientRoutes = require('./clients/clientRoutes');
const server = express();
server.use('/users', userRoutes);
server.use('/products', productRoutes);
server.use('/clients', clientRoutes);
server.listen(8000, () => console.log('API running on port 8000'));
Much cleaner, we added three sets of endpoints to our server, where each needs only two lines of easy-to-read code.
There is an alternative syntax for writing route handlers, but we'll leave that for you to explore.
Also, note that it is possible to have a central router representing our API and import the routes. This logic cleans up our main server file even more. Let's see a quick example of that.
const express = require('express');
const apiRoutes = require('./api/apiRoutes');
const server = express();
server.use('/api', userRoutes);
server.listen(8000, () => console.log('API running on port 8000'));
And the apiRoutes could look like this:
// inside /api/apiRoutes.js <- this can be place anywhere and called anything
const express = require('express');
// if the other routers are not nested inside /api then the paths would change
const userRoutes = require('./users/userRoutes');
const productRoutes = require('./products/productRoutes');
const clientRoutes = require('./clients/clientRoutes');
const router = express.Router(); // notice the Uppercase R
// this file will only be used when the route begins with "/api"
// so we can remove that from the URLs, so "/api/users" becomes simply "/users"
router.use('/users', userRoutes);
router.use('/products', productRoutes);
router.use('/clients', clientRoutes);
// .. and any other endpoint related to the user's resource
// after the route has been fully configured, we then export it so it can be required where needed
module.exports = router; // standard convention dictates that this is the last line on the file
As you can see, routers can use other routers.
The userRoutes, productRoutes, and clientRoutes remain unchanged (other than relocating them inside the API folder).
In the next section, follow along as we practice using routers.
How to Build It
Let's implement a simple API that returns strings but takes advantage of Express routers. Express routers are overkill for such a simple application, but in larger applications there would be many benefits to taking this approach.
We'll build it from scratch. First, follow these steps to create the folder and main server file:
- Create an empty folder for our Web API. Feel free to name it anything you'd like.
- CD into the folder you just created and type
npm init -y
to generate a default package.json file. The -y flag saves time by answering yes to all the questions that the npm init command would ask one at a time. - Open the folder in your favorite text editor.
- Inside the package.json file, change
"test": "echo \"Error: no test specified\" && exit 1"
inside the scripts object to read:"start": "nodemon index.js"
. This will let us run our server using nodemon by typing npm start at the command line/terminal. Make sure to save the file. - Install nodemon as a development-time dependency only (it's not needed in production) by typing:
npm install -D nodemon
- Create a file called index.js to host the server code.
Add the basic code to create our Express server with a default / endpoint:
const express = require('express');
const server = express();
server.use('/', (req, res) => res.send('API up and running!'));
// using port 9000 for this example
server.listen(9000, () => console.log('API running on port 9000'));
Then:
- Add express:
npm install express
- Start the server:
npm start
- Test it at: http://localhost:9000
Let's add our first router to manage the races resource:
- Create a folder called races
- Create raceRoutes.js inside it with this code:
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
const races = ['human', 'elf', 'hobbit', 'wizard', 'dwarf', 'orc'];
res.status(200).json(races);
});
module.exports = router;
Now modify index.js to use the new router:
const express = require('express');
const raceRoutes = require('./races/raceRoutes');
const server = express();
server.use('/races', raceRoutes);
server.use('/', (req, res) => res.send('API up and running!'));
server.listen(9000, () => console.log('API running on port 9000'));
Visiting http://localhost:9000/races should now return our array of races.
Great job! You are on your way to writing well-structured APIs that other team members (including your future self) will love using!
Guided Project
Server-Side Routing with Express Starter Code
Server-Side Routing with Express Solution
Module 2 Project: Intro to Node.js & Express
The module project contains advanced problems that will challenge and stretch your understanding of the module's content. The project has built-in tests for you to check your work, and the solution video is available in case you need help or want to see how we solved each challenge, but remember, there is always more than one way to solve a problem. Before reviewing the solution video, be sure to attempt the project and try solving the challenges yourself.
Instructions
The link below takes you to Bloom's code repository of the assignment. You'll need to fork the repo to your own GitHub account, and clone it down to your computer:
Starter Repo: Node API 2
- Fork the repository,
- clone it to your machine, and
- open the README.md file in VSCode, where you will find instructions on completing this Project.
- submit your completed project to the BloomTech Portal