Module 16: JavaScript Backend

Advanced Routing Techniques in Express.js

Understanding Express Routing Architecture

Routing in Express is the mechanism by which application endpoints (URIs) respond to client requests. While we've explored basic routing, Express offers a sophisticated routing system that enables clean, maintainable API design.

Analogy: Think of Express routing as an advanced mail sorting system. As letters (requests) arrive, they are sorted by their address (URL), type (HTTP method), and even contents (parameters and headers). The sorting system directs each letter to the right department (handler function) that can process it properly.

graph TD A[Client Request] --> B[Express Application] B --> C{Route Matching} C -->|GET /users| D[List Users Handler] C -->|POST /users| E[Create User Handler] C -->|GET /users/:id| F[Get User Handler] C -->|PUT /users/:id| G[Update User Handler] C -->|DELETE /users/:id| H[Delete User Handler] C -->|No Match| I[404 Handler] D & E & F & G & H & I --> J[Response Sent] J --> K[Client]

Route Parameters

Route parameters are named URL segments used to capture values at specific positions in the URL. These parameters are stored in the req.params object.


// Basic route parameters
app.get('/users/:userId', (req, res) => {
  console.log(req.params.userId);
  // If URL is /users/123, req.params.userId would be "123"
  res.send(`User ID: ${req.params.userId}`);
});

// Multiple parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.send(`User ID: ${userId}, Post ID: ${postId}`);
});

// Optional parameters
app.get('/products/:category/:productId?', (req, res) => {
  const { category, productId } = req.params;
  
  if (productId) {
    res.send(`Product ${productId} in category ${category}`);
  } else {
    res.send(`All products in category ${category}`);
  }
});
            

Parameter Format Validation with Regular Expressions

You can use regular expressions to validate parameter formats directly in your route definition:


// Only accept numeric IDs
app.get('/users/:userId(\\d+)', (req, res) => {
  // req.params.userId will only contain digits
  res.send(`User with numeric ID: ${req.params.userId}`);
});

// Accept specific string patterns
app.get('/products/:category([a-zA-Z]+)', (req, res) => {
  // req.params.category will only contain letters
  res.send(`Products in category: ${req.params.category}`);
});

// Multiple regular expression patterns
app.get('/files/:filename([a-zA-Z0-9_]+\\.[a-zA-Z0-9]+)', (req, res) => {
  // Will match filenames like "document.pdf" but not "doc/file.pdf"
  res.send(`File: ${req.params.filename}`);
});

// Accept specific values using alternation
app.get('/status/:state(active|inactive|pending)', (req, res) => {
  // req.params.state will only be one of: active, inactive, or pending
  res.send(`Status: ${req.params.state}`);
});
            

Real-world example: GitHub's API uses parameter pattern validation extensively. For example, their repository endpoints like /repos/:owner/:repo ensure that :owner and :repo follow GitHub's naming conventions, preventing requests with invalid characters in repository names from even reaching the handler logic.

Parameter Processing Middleware

Express provides the app.param() method to add middleware triggered specifically when certain parameters are present in a route. This enables parameter pre-processing before route handlers execute.


// Parameter middleware for user ID
app.param('userId', (req, res, next, userId) => {
  console.log(`Looking up user: ${userId}`);
  
  // Fetch user from database
  // For demonstration, we're using mock data
  const users = {
    '123': { id: '123', name: 'Alice', role: 'admin' },
    '456': { id: '456', name: 'Bob', role: 'user' }
  };
  
  const user = users[userId];
  
  if (!user) {
    // User not found
    return res.status(404).json({ error: 'User not found' });
  }
  
  // Attach user to request object
  req.user = user;
  
  // Continue to the actual route handler
  next();
});

// Routes using userId parameter
app.get('/users/:userId', (req, res) => {
  // req.user is already populated by the param middleware
  res.json(req.user);
});

app.get('/users/:userId/profile', (req, res) => {
  // Also uses the same parameter middleware
  res.json({
    user: req.user,
    profile: { /* additional profile data */ }
  });
});
            

Multiple Parameter Handlers

You can chain parameter handlers to process related resources:


// User parameter middleware
app.param('userId', (req, res, next, userId) => {
  // Fetch user...
  req.user = { id: userId, name: 'Example User' };
  next();
});

// Post parameter middleware - depends on user
app.param('postId', (req, res, next, postId) => {
  // Ensure user exists (from previous middleware)
  if (!req.user) {
    return res.status(500).json({ error: 'User middleware not executed' });
  }
  
  // Fetch post
  // Check if post belongs to the user
  const post = { id: postId, title: 'Example Post', authorId: req.user.id };
  
  if (post.authorId !== req.user.id) {
    return res.status(403).json({ error: 'Not authorized to access this post' });
  }
  
  req.post = post;
  next();
});

// Route using both parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
  // Both req.user and req.post are populated
  res.json({
    user: req.user,
    post: req.post
  });
});
            

Analogy: Parameter middleware is like a pre-processing station at a factory. Before items (requests) reach the main assembly line (route handlers), they go through specialized stations that prepare specific components (parameters). These stations validate components, add necessary parts, and reject defective items before they reach the main production process.

Query Parameters

While route parameters are part of the URL path, query parameters are appended to the URL after a question mark. Express automatically parses these parameters into the req.query object.


// Basic query parameters
// URL: /search?q=express&limit=10
app.get('/search', (req, res) => {
  const { q, limit } = req.query;
  res.send(`Searching for "${q}" with limit ${limit || 'default'}`);
});

// Multiple values for the same parameter
// URL: /filter?tags=javascript&tags=express&tags=node
app.get('/filter', (req, res) => {
  const tags = req.query.tags;
  
  // Check if tags is an array or a single value
  const tagsList = Array.isArray(tags) ? tags : [tags];
  
  res.json({
    filtering: {
      tags: tagsList
    }
  });
});
            

Advanced Query Parameter Processing

Query parameters are often used for filtering, sorting, and pagination in API design:


// Advanced query parameter processing
app.get('/api/products', (req, res) => {
  // Extract and parse query parameters
  const {
    category,
    minPrice,
    maxPrice,
    sort = 'name',
    order = 'asc',
    page = 1,
    limit = 20
  } = req.query;
  
  // Convert string values to appropriate types
  const pageNum = parseInt(page, 10);
  const limitNum = parseInt(limit, 10);
  const minPriceNum = minPrice ? parseFloat(minPrice) : undefined;
  const maxPriceNum = maxPrice ? parseFloat(maxPrice) : undefined;
  
  // Build filter object
  const filter = {};
  
  if (category) {
    filter.category = category;
  }
  
  if (minPriceNum !== undefined || maxPriceNum !== undefined) {
    filter.price = {};
    if (minPriceNum !== undefined) filter.price.$gte = minPriceNum;
    if (maxPriceNum !== undefined) filter.price.$lte = maxPriceNum;
  }
  
  // Build sort object
  const sortObj = {};
  sortObj[sort] = order === 'desc' ? -1 : 1;
  
  // Calculate pagination
  const skip = (pageNum - 1) * limitNum;
  
  // Fetch data from database (mock implementation)
  const products = fetchProducts(filter, sortObj, skip, limitNum);
  const total = countProducts(filter);
  
  // Return formatted response
  res.json({
    data: products,
    pagination: {
      page: pageNum,
      limit: limitNum,
      total,
      pages: Math.ceil(total / limitNum)
    }
  });
});

// Helper functions (mock implementations)
function fetchProducts(filter, sort, skip, limit) {
  // In a real app, this would query a database
  return [
    { id: 1, name: 'Product 1', price: 99.99, category: 'electronics' },
    { id: 2, name: 'Product 2', price: 149.99, category: 'electronics' }
  ];
}

function countProducts(filter) {
  // In a real app, this would count matched documents
  return 100;
}
            

Real-world example: E-commerce platforms like Amazon use extensive query parameter processing for their product search APIs. When you filter products by price range, sort by customer ratings, or paginate through search results, each of these operations corresponds to query parameters that their backend routes process to deliver the exact data subset you requested.

Route Handlers

Express routes can have multiple handler functions, which behave like middleware specific to that route. These handlers are executed in sequence and can share data via the request object.

Multiple Handler Functions


// Route with multiple handler functions
app.get('/complex-route',
  // First handler - authentication check
  (req, res, next) => {
    console.log('Authentication check');
    // Mock authentication
    req.user = { id: 123, role: 'admin' };
    next();
  },
  // Second handler - authorization check
  (req, res, next) => {
    console.log('Authorization check');
    if (req.user.role !== 'admin') {
      return res.status(403).json({ error: 'Admin access required' });
    }
    next();
  },
  // Third handler - data preparation
  (req, res, next) => {
    console.log('Data preparation');
    req.contextData = {
      timestamp: Date.now(),
      environment: process.env.NODE_ENV
    };
    next();
  },
  // Final handler - sends response
  (req, res) => {
    console.log('Final handler');
    res.json({
      user: req.user,
      data: req.contextData,
      message: 'Complex route processed successfully'
    });
  }
);
            

Handler Arrays

You can also use arrays of handler functions for better organization:


// Middleware functions
const checkAuth = (req, res, next) => {
  // Authentication logic
  req.user = { id: 123, role: 'user' };
  next();
};

const checkAdmin = (req, res, next) => {
  if (!req.user || req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Admin access required' });
  }
  next();
};

const validateInput = (req, res, next) => {
  // Input validation logic
  if (!req.body.name) {
    return res.status(400).json({ error: 'Name is required' });
  }
  next();
};

const logRequest = (req, res, next) => {
  console.log(`Request to ${req.method} ${req.path}`);
  next();
};

// Route with array of handlers
app.post('/api/admin/settings',
  [logRequest, checkAuth, checkAdmin, validateInput],
  (req, res) => {
    // Main route handler
    res.json({ success: true, message: 'Settings updated' });
  }
);
            
sequenceDiagram participant Client participant Express participant Auth as Authentication Handler participant Admin as Authorization Handler participant Validation as Validation Handler participant Main as Main Handler Client->>Express: POST /api/admin/settings Express->>Auth: Execute first handler Auth->>Admin: next() alt User is Admin Admin->>Validation: next() alt Valid Input Validation->>Main: next() Main->>Express: Send success response Express->>Client: 200 OK else Invalid Input Validation->>Express: Send validation error Express->>Client: 400 Bad Request end else User is not Admin Admin->>Express: Send authorization error Express->>Client: 403 Forbidden end

Analogy: Multiple route handlers are like security checkpoints at an airport. Each passenger (request) must pass through several checkpoints in sequence: ID verification (authentication), security screening (validation), customs (authorization), and finally boarding (main handler). Each checkpoint can either clear the passenger to proceed or deny further progress.

Route Methods

Express provides route methods corresponding to HTTP methods. Here's how to use them effectively:

Basic HTTP Methods


// GET - Retrieve data
app.get('/users', (req, res) => {
  res.json({ users: [/* user data */] });
});

// POST - Create data
app.post('/users', (req, res) => {
  // Create a new user from req.body
  res.status(201).json({ message: 'User created', user: newUser });
});

// PUT - Replace data
app.put('/users/:userId', (req, res) => {
  // Replace entire user with req.body
  res.json({ message: 'User replaced', user: updatedUser });
});

// PATCH - Update data
app.patch('/users/:userId', (req, res) => {
  // Update specific user fields from req.body
  res.json({ message: 'User updated', user: patchedUser });
});

// DELETE - Remove data
app.delete('/users/:userId', (req, res) => {
  // Delete user
  res.json({ message: 'User deleted' });
});
            

Less Common HTTP Methods


// HEAD - Like GET but returns only headers, no body
app.head('/status', (req, res) => {
  // Set headers
  res.setHeader('X-API-Version', '1.0');
  res.setHeader('X-Rate-Limit-Remaining', '98');
  res.end();
});

// OPTIONS - Describe communication options
app.options('/api/users', (req, res) => {
  res.setHeader('Allow', 'GET, POST, PUT, PATCH, DELETE');
  res.end();
});
            

app.all() Method

The app.all() method matches all HTTP methods for a path:


// Matches all HTTP methods
app.all('/api/*', (req, res, next) => {
  // This runs for any HTTP method to paths starting with /api/
  console.log(`${req.method} request to ${req.path}`);
  next();
});
            

Method Chaining

Express supports method chaining for routes with the same path:


// Method chaining for the same path
app.route('/users/:userId')
  .get((req, res) => {
    res.json({ /* user data */ });
  })
  .put((req, res) => {
    res.json({ message: 'User updated' });
  })
  .delete((req, res) => {
    res.json({ message: 'User deleted' });
  });
            

Real-world example: RESTful APIs like Stripe's payment API use appropriate HTTP methods for different operations. They use POST for creating payments, GET for retrieving payment details, and PUT/PATCH for updating payment information—all following standard HTTP semantics to create a predictable, intuitive API.

Route Paths

Express route paths can be specified using strings, path patterns, or regular expressions:

String Paths


// Simple string path
app.get('/about', (req, res) => {
  res.send('About page');
});

// Path with parameters
app.get('/users/:userId', (req, res) => {
  res.send(`User ID: ${req.params.userId}`);
});
            

String Patterns


// ? makes the previous character optional
app.get('/item?', (req, res) => {
  // Matches /item and /items
  res.send('Item route');
});

// + matches one or more occurrences of the previous character
app.get('/product+', (req, res) => {
  // Matches /product, /productt, /producttt, etc.
  res.send('Product route');
});

// * acts as a wildcard
app.get('/wild*card', (req, res) => {
  // Matches /wildcard, /wildXcard, /wild123card, etc.
  res.send('Wildcard route');
});

// () groups characters
app.get('/user(name)?', (req, res) => {
  // Matches /user and /username
  res.send('User route');
});
            

Regular Expression Paths


// Match paths ending with .html
app.get(/\.html$/, (req, res) => {
  res.send('HTML file requested');
});

// Match paths that include 'blog'
app.get(/blog/, (req, res) => {
  res.send('Blog route');
});

// Match specific pattern with capturing groups
app.get(/^\/posts\/(\d{4})\/(\d{2})$/, (req, res) => {
  // Matches paths like /posts/2023/05
  // Capture groups are available in req.params array
  const year = req.params[0];
  const month = req.params[1];
  res.send(`Posts from ${month}/${year}`);
});
            
Express Route Path Matching String Paths Exact Matching '/users' → /users String Patterns Simple wildcards '/item?' → /item, /items Regular Expressions Complex patterns /\.html$/ → files.html Route Parameters '/users/:userId' → /users/123 Accessed via req.params.userId Regex with Capture Groups /^\/posts\/(\d{4})\/(\d{2})$/ Accessed via req.params[0], req.params[1]

Route Ordering

The order in which routes are defined is significant. Express matches routes in the order they are added to the application.


// Route ordering example

// This route will never be reached for /users/profile
app.get('/users/:userId', (req, res) => {
  res.send(`User ID: ${req.params.userId}`);
});

// This specific route should be defined first
app.get('/users/profile', (req, res) => {
  res.send('User profile');
});

// Correct ordering
app.get('/users/profile', (req, res) => {
  res.send('User profile');
});

app.get('/users/:userId', (req, res) => {
  res.send(`User ID: ${req.params.userId}`);
});

// More specific routes should come before generic ones
app.get('/items/new', specificHandler);
app.get('/items/:category/special', specificHandler);
app.get('/items/:category', categoryHandler);
app.get('/items/:itemId(\\d+)', itemHandler);
            

Route-specific Middleware

Middleware can be applied to specific routes, allowing for targeted preprocessing:


// Authentication middleware
const authenticate = (req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  // Verify token...
  req.user = { id: 123, role: 'user' };
  next();
};

// Log only specific routes
const logRoute = (req, res, next) => {
  console.log(`Access to protected route: ${req.path}`);
  next();
};

// Public route - no authentication
app.get('/api/public', (req, res) => {
  res.json({ message: 'Public data' });
});

// Protected route - with authentication
app.get('/api/protected',
  authenticate,  // Apply authentication middleware
  logRoute,      // Apply logging middleware
  (req, res) => {
    res.json({ message: 'Protected data', user: req.user });
  }
);
            

Analogy: Route ordering is like a series of sieves with decreasing mesh sizes. The first sieve (most specific routes) catches large particles, and each subsequent sieve (more generic routes) catches smaller ones. If you put the fine mesh sieve first, it would catch everything, preventing the other sieves from serving their purpose.

Advanced Route Pattern Recognition

For complex applications, combining different route matching techniques can create sophisticated routing systems:

Hybrid Route Patterns


// Regex parameters with validation and custom formats
app.get('/users/:userId([0-9a-f]{24})/posts/:year(\\d{4})', (req, res) => {
  // Matches MongoDB ObjectId format for userId and 4-digit year
  const { userId, year } = req.params;
  res.send(`Posts from user ${userId} in year ${year}`);
});

// Optional segments with parameters
app.get('/products/:category?/:subcategory?', (req, res) => {
  const { category, subcategory } = req.params;
  
  if (category && subcategory) {
    res.send(`Products in ${category} / ${subcategory}`);
  } else if (category) {
    res.send(`Products in ${category}`);
  } else {
    res.send('All products');
  }
});

// Path wildcards with parameters
app.get('/files/:username/*', (req, res) => {
  // req.params[0] contains the rest of the URL after /files/:username/
  const filepath = req.params[0];
  res.send(`File ${filepath} for user ${req.params.username}`);
});
            

Custom Path Matching Logic

For extremely complex routing requirements, you can implement custom path matching logic:


// Custom path matching using middleware
function customPathMatcher(pattern) {
  return (req, res, next) => {
    // Example: Match paths with specific segment counts
    const segments = req.path.split('/').filter(Boolean);
    
    if (pattern === 'three-segments' && segments.length === 3) {
      req.customPattern = {
        type: pattern,
        segments
      };
      return next('route'); // Skip to the next route, not middleware
    }
    
    next(); // Continue to next middleware
  };
}

// Apply custom path matcher
app.use(customPathMatcher('three-segments'));

// Route that handles the custom pattern
app.get('*', (req, res) => {
  if (req.customPattern && req.customPattern.type === 'three-segments') {
    const [section, subsection, item] = req.customPattern.segments;
    res.send(`Section: ${section}, Subsection: ${subsection}, Item: ${item}`);
  } else {
    next(); // Pass to next route handler
  }
});

// Default route
app.get('*', (req, res) => {
  res.status(404).send('Not found');
});
            

Real-world example: Content management systems like WordPress use advanced route pattern recognition to handle different content types and URL structures. They might map URLs like /blog/2023/05/title-slug to blog posts from a specific date, while /products/category/item-slug maps to products within categories, each with their own handling logic.

Response Methods

Express provides various response methods to send different types of responses:


// Basic response methods
app.get('/api/text', (req, res) => {
  res.send('Plain text response');
});

app.get('/api/html', (req, res) => {
  res.send('<h1>HTML response</h1>');
});

app.get('/api/json', (req, res) => {
  res.json({ message: 'JSON response' });
});

app.get('/api/status', (req, res) => {
  res.sendStatus(201); // Sends "Created" with 201 status
});

// Chaining status with response
app.get('/api/error', (req, res) => {
  res.status(400).json({ error: 'Bad request' });
});

// Sending files
app.get('/download', (req, res) => {
  res.download('./files/report.pdf', 'user-report.pdf');
});

// Sending files with options
app.get('/download-options', (req, res) => {
  res.download('./files/report.pdf', 'user-report.pdf', {
    headers: {
      'x-timestamp': Date.now()
    }
  }, (err) => {
    if (err) {
      // Handle error, but don't try to send response here
      console.error('Download error:', err);
    } else {
      // File was sent successfully
      console.log('File downloaded');
    }
  });
});

// End response without data
app.get('/api/empty', (req, res) => {
  res.status(204).end();
});

// Redirect response
app.get('/old-page', (req, res) => {
  res.redirect('/new-page');
});

// Redirect with status code
app.get('/permanent-redirect', (req, res) => {
  res.redirect(301, '/new-location');
});
            
flowchart LR A[Route Handler] --> B{Response Type?} B -->|Plain Text| C[res.send] B -->|JSON Data| D[res.json] B -->|File Download| E[res.download] B -->|Status Only| F[res.sendStatus] B -->|Redirect| G[res.redirect] B -->|No Content| H[res.end] B -->|HTML| I[res.render] C & D & E & F & G & H & I --> J[HTTP Response]

Analogy: Express response methods are like different types of packaging for products in a warehouse. Depending on what's being shipped (data type), you choose the appropriate packaging: plain boxes (text), specialized containers (JSON), shipping tubes (files), or even redirection labels (redirects) that send the recipient to a different address.

Practice Activities

Activity 1: Advanced Routing Patterns

Create an Express application that demonstrates advanced route patterns:

  • Routes with regular expression parameters
  • Routes with multiple optional parameters
  • Routes with wildcard patterns
  • Parameter validation using regex patterns

Test the routes with various URL patterns and observe how pattern matching works.

Activity 2: Parameter Middleware

Implement a comprehensive parameter middleware system:

  • Create middleware for processing common parameters (userId, productId, etc.)
  • Implement validation and type conversion in the parameter middleware
  • Handle dependencies between parameters (e.g., posts that belong to users)
  • Add error handling for invalid or unauthorized parameter access

Create routes that use these parameters and test with valid and invalid values.

Activity 3: Response Formatting

Create a complete API with consistent response formatting:

  • Implement standard JSON response structure for all endpoints
  • Add proper status codes for different response types
  • Include pagination, sorting, and filtering via query parameters
  • Create endpoints that demonstrate different response methods (json, file downloads, redirects)

Test the API with tools like Postman or curl to verify consistent response formatting.

Key Takeaways

In the next lecture, we'll explore the Express Router object for modularizing routes and creating maintainable application structures.