Phase 1: API Routes (With Detailed Comments)

← Back to Main Tutorial

Understanding the Problem

In this phase, we need to set up the API routes for our Express application. The main purpose of our Express application is to serve as a REST API server, with all API routes beginning with the "/api/" prefix.

Our goal is to create a structured router setup that will allow us to organize our endpoints logically and test that the API routing system works correctly.

Why structure matters: A well-organized API structure is essential for maintainability. By grouping related endpoints under logical prefixes (e.g., '/api/users', '/api/products'), you make the codebase more intuitive and easier to navigate as it grows.

REST API principles: RESTful APIs organize endpoints around resources and use HTTP methods (GET, POST, PUT, DELETE) to perform operations on those resources. This pattern creates a predictable, consistent interface for clients.

Planning the Solution

  1. Create an API router structure
  2. Connect the API router to the main router
  3. Add a test endpoint to verify that the API routing works
  4. Test the endpoint with fetch requests

Implementing the Solution

1. Create API Router Structure

First, let's create an api folder inside the routes folder and add an index.js file.

mkdir -p backend/routes/api
touch backend/routes/api/index.js

Add the following code to backend/routes/api/index.js to create a basic Express router:

// backend/routes/api/index.js
const router = require('express').Router();     // Create a new Express router instance

module.exports = router;                        // Export the router for use in other files

Express Router: The Express Router is like a mini-application capable of performing middleware and routing functions. It's particularly useful for creating modular, mountable route handlers.

Modular design: By creating separate router files, you can:

  • Keep related routes together in dedicated files
  • Apply middleware to specific groups of routes
  • Import and use these routers in different parts of your application

This approach follows the separation of concerns principle, which makes the codebase easier to maintain.

2. Connect API Router to Main Router

Now, let's connect this API router to our main router in backend/routes/index.js. Update the file to include the API router:

// backend/routes/index.js
const express = require('express');
const router = express.Router();               // Create main router
const apiRouter = require('./api');            // Import the API router

// Add a XSRF-TOKEN cookie
router.get("/api/csrf/restore", (req, res) => {
  const csrfToken = req.csrfToken();          // Generate new CSRF token
  res.cookie("XSRF-TOKEN", csrfToken);        // Set it as a cookie
  res.status(200).json({
    'XSRF-Token': csrfToken                   // Also include it in the JSON response
  });
});

// Connect the API router - all routes in apiRouter will be prefixed with '/api'
router.use('/api', apiRouter);                // Mount the API router under the /api path

// Remove this test route since we're no longer using it
// router.get('/hello/world', function(req, res) {
//   res.cookie('XSRF-TOKEN', req.csrfToken());
//   res.send('Hello World!');
// });

module.exports = router;                      // Export the main router

Router Mounting: The line router.use('/api', apiRouter) mounts the API router at the path '/api'. This means that any routes defined in apiRouter will automatically be prefixed with '/api'.

Router Hierarchy: Express routers can be nested, creating a hierarchy of routes. This pattern allows for:

  • Logical grouping of related endpoints
  • Application of specific middleware to groups of routes
  • Clear organization of route handlers

CSRF Token Restoration: The /api/csrf/restore endpoint provides a way for the frontend to get a new CSRF token. This is particularly useful after the session expires or when a user first visits the site.

3. Add a Test Route to API Router

Let's add a test route to verify that our API router is working correctly. Update backend/routes/api/index.js to include a test POST route:

// backend/routes/api/index.js
const router = require('express').Router();

// Test route to check if API router is working
router.post('/test', function(req, res) {      // Define a POST endpoint at /api/test
  res.json({ requestBody: req.body });         // Echo back the request body as JSON
});

module.exports = router;

Route Handler: The function function(req, res) {...} is a route handler that processes incoming requests to the '/test' endpoint.

Request and Response:

  • req (request) object contains information about the HTTP request, including body, parameters, headers, etc.
  • res (response) object provides methods to send responses back to the client

res.json(): This method sends a JSON response with the provided object. It automatically sets the Content-Type header to application/json.

req.body: Contains key-value pairs of data submitted in the request body. This is available because we set up the express.json() middleware in app.js which parses JSON request bodies.

4. Test the API Router

Now that we have set up our API router, let's test it to make sure it's working correctly. Start your server if it's not already running:

cd backend
npm start

To test the API route, we need to make a POST request with a JSON body and include the CSRF token. Here's how to do it:

  1. First, open your browser and navigate to http://localhost:8000/api/csrf/restore to get a new CSRF token.
  2. Open your browser's developer tools (F12 or right-click > Inspect).
  3. Go to the Console tab.
  4. Look for the XSRF-TOKEN cookie in the Application tab > Cookies > localhost.
  5. Use the following fetch request in the console, replacing the XSRF-TOKEN value with your actual token:
fetch('/api/test', {
  method: "POST",                               // HTTP method
  headers: {
    "Content-Type": "application/json",         // Specify content type as JSON
    "XSRF-TOKEN": `<value of XSRF-TOKEN cookie>`  // Include CSRF token
  },
  body: JSON.stringify({ hello: 'world' })      // Convert JS object to JSON string
}).then(res => res.json()).then(data => console.log(data));

Fetch API: The Fetch API provides a modern interface for making HTTP requests. It returns a Promise that resolves to the Response object representing the response to the request.

CSRF Protection Flow:

  1. Server generates a CSRF token and sends it to the client in a cookie named XSRF-TOKEN
  2. Client includes this token in the XSRF-TOKEN header when making non-GET requests
  3. The csurf middleware validates that the token in the header matches the one stored in the session
  4. If the tokens match, the request proceeds; otherwise, it's rejected with a 403 Forbidden error

JSON.stringify(): Converts a JavaScript object to a JSON string, necessary because the fetch body must be a string, not an object.

If everything is set up correctly, you should see the following response in the console:

{ requestBody: { hello: 'world' } }

This confirms that your API router is working properly and can handle POST requests with JSON bodies.

Review the Solution

Let's review what we've accomplished in this phase:

This API router structure will provide a solid foundation for adding more specific API endpoints in the future, such as user authentication routes, application-specific routes, and more.

Architecture benefits: The router structure we've created provides several benefits:

  • Modularity: Each resource can have its own router file, making the code more organized
  • Maintainability: Easier to find and update specific endpoints
  • Scalability: New resources can be added without modifying existing code
  • Middleware scoping: Apply specific middleware only to certain groups of routes

API versioning: This structure also makes it easy to implement API versioning (e.g., /api/v1/users, /api/v2/users) if needed in the future.

Real-world Application

In real-world applications, this type of router structure is commonly used to organize API endpoints logically. For example:

Each of these could have their own router file, making the codebase more maintainable and easier to understand.

Example resource router structure:

backend/
└── routes/
    ├── index.js             # Main router
    └── api/
        ├── index.js         # API router
        ├── users.js         # User routes (/api/users/...)
        ├── products.js      # Product routes (/api/products/...)
        └── orders.js        # Order routes (/api/orders/...)

In the api/index.js file, you would connect these routers:

// backend/routes/api/index.js
const router = require('express').Router();
const usersRouter = require('./users');
const productsRouter = require('./products');
const ordersRouter = require('./orders');

router.use('/users', usersRouter);
router.use('/products', productsRouter);
router.use('/orders', ordersRouter);

module.exports = router;

Common Issues and Solutions

Debugging routes: When troubleshooting route issues, consider these approaches:

  • Add console.log statements in your route handlers to confirm they're being called
  • Use middleware to log all incoming requests: app.use((req, res, next) => { console.log(req.method, req.path); next(); });
  • Check that routers are properly exported and imported
  • Verify that middleware order is correct (e.g., parsing middleware before routes)

CSRF troubleshooting: The most common CSRF issues arise when:

  • The token in the header doesn't match the token in the cookie
  • The CSRF cookie has expired
  • The header name doesn't match what the csurf middleware expects

Next Steps

Now that you have set up the API routes for your application, you are ready to move on to Phase 2: Error Handling, where you will implement error-handling middleware to make your application more robust.

In the subsequent phases, we'll build upon this API router to add user authentication routes and other features.

Looking ahead: In the next phase, you'll learn how to:

  • Create custom error classes for different types of errors
  • Implement error-handling middleware to catch and process errors
  • Send appropriate error responses to clients
  • Handle validation errors, authentication errors, and other common error scenarios

Error handling is a critical part of any robust API, as it ensures that when things go wrong, clients receive meaningful information about what happened and why.