Skip to content

Routing and Navigation

Overview

Routing is the mechanism that determines how an application responds to client requests for specific endpoints. This chapter covers HTTP routing in Node.js, from basic routing with the built-in http module to advanced routing with Express.js.

Basic HTTP Routing

Manual Routing with HTTP Module

javascript
// basic-routing.js
const http = require('http');
const url = require('url');
const querystring = require('querystring');

class BasicRouter {
  constructor() {
    this.routes = new Map();
  }

  addRoute(method, path, handler) {
    const key = `${method.toUpperCase()}:${path}`;
    this.routes.set(key, handler);
  }

  get(path, handler) {
    this.addRoute('GET', path, handler);
  }

  post(path, handler) {
    this.addRoute('POST', path, handler);
  }

  put(path, handler) {
    this.addRoute('PUT', path, handler);
  }

  delete(path, handler) {
    this.addRoute('DELETE', path, handler);
  }

  async handleRequest(req, res) {
    const parsedUrl = url.parse(req.url, true);
    const path = parsedUrl.pathname;
    const method = req.method;
    const query = parsedUrl.query;

    // Add query parameters to request
    req.query = query;

    // Parse body for POST/PUT requests
    if (method === 'POST' || method === 'PUT') {
      req.body = await this.parseBody(req);
    }

    // Find matching route
    const routeKey = `${method}:${path}`;
    const handler = this.routes.get(routeKey);

    if (handler) {
      try {
        await handler(req, res);
      } catch (error) {
        this.handleError(res, error);
      }
    } else {
      this.handle404(res);
    }
  }

  async parseBody(req) {
    return new Promise((resolve, reject) => {
      let body = '';
      
      req.on('data', chunk => {
        body += chunk.toString();
      });

      req.on('end', () => {
        try {
          const contentType = req.headers['content-type'];
          
          if (contentType && contentType.includes('application/json')) {
            resolve(JSON.parse(body));
          } else if (contentType && contentType.includes('application/x-www-form-urlencoded')) {
            resolve(querystring.parse(body));
          } else {
            resolve(body);
          }
        } catch (error) {
          reject(error);
        }
      });

      req.on('error', reject);
    });
  }

  handleError(res, error) {
    res.writeHead(500, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      error: 'Internal Server Error',
      message: error.message
    }));
  }

  handle404(res) {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      error: 'Not Found',
      message: 'Route not found'
    }));
  }
}

// Usage example
const router = new BasicRouter();

// Define routes
router.get('/', (req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'Welcome to the API' }));
});

router.get('/users', (req, res) => {
  const { page = 1, limit = 10 } = req.query;
  
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    users: [
      { id: 1, name: 'John Doe' },
      { id: 2, name: 'Jane Smith' }
    ],
    pagination: { page: parseInt(page), limit: parseInt(limit) }
  }));
});

router.post('/users', (req, res) => {
  const userData = req.body;
  
  // Simulate user creation
  const newUser = {
    id: Date.now(),
    ...userData,
    createdAt: new Date().toISOString()
  };

  res.writeHead(201, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(newUser));
});

// Create server
const server = http.createServer((req, res) => {
  router.handleRequest(req, res);
});

server.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Express.js Routing

Basic Express Routing

javascript
// express-basic-routing.js
const express = require('express');
const app = express();

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Basic routes
app.get('/', (req, res) => {
  res.json({ message: 'Welcome to Express API' });
});

// Route with parameters
app.get('/users/:id', (req, res) => {
  const { id } = req.params;
  
  res.json({
    user: {
      id: parseInt(id),
      name: `User ${id}`,
      email: `user${id}@example.com`
    }
  });
});

// Route with query parameters
app.get('/search', (req, res) => {
  const { q, category, page = 1, limit = 10 } = req.query;
  
  res.json({
    query: q,
    category,
    results: [],
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit)
    }
  });
});

// Multiple route parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  
  res.json({
    post: {
      id: parseInt(postId),
      userId: parseInt(userId),
      title: `Post ${postId} by User ${userId}`
    }
  });
});

// Route with optional parameters
app.get('/products/:category/:subcategory?', (req, res) => {
  const { category, subcategory } = req.params;
  
  res.json({
    category,
    subcategory: subcategory || 'all',
    products: []
  });
});

// Wildcard routes
app.get('/files/*', (req, res) => {
  const filePath = req.params[0];
  
  res.json({
    message: `Accessing file: ${filePath}`,
    fullPath: `/files/${filePath}`
  });
});

app.listen(3000, () => {
  console.log('Express server running on http://localhost:3000');
});

Advanced Express Routing

javascript
// express-advanced-routing.js
const express = require('express');
const app = express();

app.use(express.json());

// Route-specific middleware
const authenticateUser = (req, res, next) => {
  const token = req.headers.authorization;
  
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  
  // Simulate token validation
  req.user = { id: 1, name: 'John Doe' };
  next();
};

const validateUserData = (req, res, next) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
  
  next();
};

// Routes with middleware
app.get('/protected', authenticateUser, (req, res) => {
  res.json({
    message: 'This is a protected route',
    user: req.user
  });
});

app.post('/users', validateUserData, (req, res) => {
  const userData = req.body;
  
  res.status(201).json({
    message: 'User created successfully',
    user: {
      id: Date.now(),
      ...userData,
      createdAt: new Date().toISOString()
    }
  });
});

// Route handlers with multiple functions
app.get('/multi-handler',
  (req, res, next) => {
    console.log('First handler');
    req.customData = 'Hello from first handler';
    next();
  },
  (req, res, next) => {
    console.log('Second handler');
    req.customData += ' and second handler';
    next();
  },
  (req, res) => {
    res.json({ message: req.customData });
  }
);

// Route parameter validation
app.param('userId', (req, res, next, userId) => {
  const id = parseInt(userId);
  
  if (isNaN(id) || id <= 0) {
    return res.status(400).json({ error: 'Invalid user ID' });
  }
  
  req.userId = id;
  next();
});

app.get('/users/:userId', (req, res) => {
  res.json({
    user: {
      id: req.userId,
      name: `User ${req.userId}`
    }
  });
});

// Error handling for specific routes
app.get('/error-demo', (req, res, next) => {
  const error = new Error('Something went wrong');
  error.status = 500;
  next(error);
});

// Route-level error handler
app.use('/api', (error, req, res, next) => {
  res.status(error.status || 500).json({
    error: error.message,
    path: req.path
  });
});

app.listen(3000);

Router Module

Creating Modular Routes

javascript
// routes/users.js
const express = require('express');
const router = express.Router();

// Middleware specific to this router
router.use((req, res, next) => {
  console.log('Users router middleware');
  req.timestamp = new Date().toISOString();
  next();
});

// Mock data
let users = [
  { id: 1, name: 'John Doe', email: 'john@example.com' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];

// GET /users
router.get('/', (req, res) => {
  const { page = 1, limit = 10, search } = req.query;
  
  let filteredUsers = users;
  
  if (search) {
    filteredUsers = users.filter(user => 
      user.name.toLowerCase().includes(search.toLowerCase()) ||
      user.email.toLowerCase().includes(search.toLowerCase())
    );
  }
  
  const startIndex = (page - 1) * limit;
  const endIndex = startIndex + parseInt(limit);
  const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
  
  res.json({
    users: paginatedUsers,
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total: filteredUsers.length,
      pages: Math.ceil(filteredUsers.length / limit)
    },
    timestamp: req.timestamp
  });
});

// GET /users/:id
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const user = users.find(u => u.id === id);
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json({ user, timestamp: req.timestamp });
});

// POST /users
router.post('/', (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
  
  const newUser = {
    id: Math.max(...users.map(u => u.id)) + 1,
    name,
    email,
    createdAt: req.timestamp
  };
  
  users.push(newUser);
  
  res.status(201).json({
    message: 'User created successfully',
    user: newUser
  });
});

// PUT /users/:id
router.put('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === id);
  
  if (userIndex === -1) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  const { name, email } = req.body;
  
  users[userIndex] = {
    ...users[userIndex],
    name: name || users[userIndex].name,
    email: email || users[userIndex].email,
    updatedAt: req.timestamp
  };
  
  res.json({
    message: 'User updated successfully',
    user: users[userIndex]
  });
});

// DELETE /users/:id
router.delete('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === id);
  
  if (userIndex === -1) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  const deletedUser = users.splice(userIndex, 1)[0];
  
  res.json({
    message: 'User deleted successfully',
    user: deletedUser
  });
});

module.exports = router;
javascript
// routes/posts.js
const express = require('express');
const router = express.Router();

let posts = [
  { id: 1, title: 'First Post', content: 'This is the first post', userId: 1 },
  { id: 2, title: 'Second Post', content: 'This is the second post', userId: 2 }
];

// GET /posts
router.get('/', (req, res) => {
  const { userId, page = 1, limit = 10 } = req.query;
  
  let filteredPosts = posts;
  
  if (userId) {
    filteredPosts = posts.filter(post => post.userId === parseInt(userId));
  }
  
  res.json({
    posts: filteredPosts,
    total: filteredPosts.length
  });
});

// GET /posts/:id
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find(p => p.id === id);
  
  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }
  
  res.json({ post });
});

// POST /posts
router.post('/', (req, res) => {
  const { title, content, userId } = req.body;
  
  if (!title || !content || !userId) {
    return res.status(400).json({ error: 'Title, content, and userId are required' });
  }
  
  const newPost = {
    id: Math.max(...posts.map(p => p.id)) + 1,
    title,
    content,
    userId: parseInt(userId),
    createdAt: new Date().toISOString()
  };
  
  posts.push(newPost);
  
  res.status(201).json({
    message: 'Post created successfully',
    post: newPost
  });
});

module.exports = router;
javascript
// app.js - Using modular routes
const express = require('express');
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');

const app = express();

// Global middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Logging middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
  next();
});

// Mount routers
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);

// Root route
app.get('/', (req, res) => {
  res.json({
    message: 'API Server',
    endpoints: {
      users: '/api/users',
      posts: '/api/posts'
    }
  });
});

// 404 handler
app.use('*', (req, res) => {
  res.status(404).json({
    error: 'Route not found',
    path: req.originalUrl
  });
});

// Error handler
app.use((error, req, res, next) => {
  console.error('Error:', error);
  
  res.status(error.status || 500).json({
    error: error.message || 'Internal Server Error',
    path: req.path
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Advanced Routing Patterns

Route Versioning

javascript
// routes/v1/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.json({
    version: 'v1',
    users: [
      { id: 1, name: 'John Doe' }
    ]
  });
});

module.exports = router;
javascript
// routes/v2/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.json({
    version: 'v2',
    data: {
      users: [
        { 
          id: 1, 
          firstName: 'John', 
          lastName: 'Doe',
          profile: {
            email: 'john@example.com'
          }
        }
      ],
      meta: {
        total: 1,
        page: 1
      }
    }
  });
});

module.exports = router;
javascript
// app-versioned.js
const express = require('express');
const app = express();

app.use(express.json());

// Version 1 routes
app.use('/api/v1/users', require('./routes/v1/users'));

// Version 2 routes
app.use('/api/v2/users', require('./routes/v2/users'));

// Default to latest version
app.use('/api/users', require('./routes/v2/users'));

// Version detection middleware
app.use('/api', (req, res, next) => {
  const version = req.headers['api-version'] || 'v2';
  req.apiVersion = version;
  next();
});

app.listen(3000);

Dynamic Route Loading

javascript
// utils/route-loader.js
const fs = require('fs');
const path = require('path');

class RouteLoader {
  constructor(app) {
    this.app = app;
    this.routes = new Map();
  }

  loadRoutes(routesDir) {
    const routeFiles = this.getRouteFiles(routesDir);
    
    for (const file of routeFiles) {
      this.loadRoute(file);
    }
  }

  getRouteFiles(dir) {
    const files = [];
    
    const readDir = (currentDir) => {
      const items = fs.readdirSync(currentDir);
      
      for (const item of items) {
        const fullPath = path.join(currentDir, item);
        const stat = fs.statSync(fullPath);
        
        if (stat.isDirectory()) {
          readDir(fullPath);
        } else if (item.endsWith('.js') && !item.startsWith('_')) {
          files.push(fullPath);
        }
      }
    };
    
    readDir(dir);
    return files;
  }

  loadRoute(filePath) {
    try {
      const routeModule = require(filePath);
      const routePath = this.getRoutePathFromFile(filePath);
      
      if (typeof routeModule === 'function') {
        // Route module exports a function that returns a router
        const router = routeModule();
        this.app.use(routePath, router);
      } else if (routeModule.router) {
        // Route module exports an object with router property
        this.app.use(routePath, routeModule.router);
      } else {
        // Route module exports a router directly
        this.app.use(routePath, routeModule);
      }
      
      this.routes.set(routePath, filePath);
      console.log(`Loaded route: ${routePath} from ${filePath}`);
    } catch (error) {
      console.error(`Failed to load route from ${filePath}:`, error.message);
    }
  }

  getRoutePathFromFile(filePath) {
    const relativePath = path.relative(path.join(__dirname, '../routes'), filePath);
    const routePath = '/' + relativePath
      .replace(/\\/g, '/') // Convert Windows paths
      .replace(/\.js$/, '') // Remove .js extension
      .replace(/\/index$/, '') // Remove /index
      .replace(/\[([^\]]+)\]/g, ':$1'); // Convert [param] to :param
    
    return routePath === '/' ? '' : routePath;
  }

  reloadRoute(routePath) {
    const filePath = this.routes.get(routePath);
    
    if (filePath) {
      // Clear require cache
      delete require.cache[require.resolve(filePath)];
      
      // Reload the route
      this.loadRoute(filePath);
    }
  }

  getLoadedRoutes() {
    return Array.from(this.routes.keys());
  }
}

module.exports = RouteLoader;

Route Guards and Middleware

javascript
// middleware/auth-guards.js
const jwt = require('jsonwebtoken');
const config = require('../config');

// Authentication guard
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  
  try {
    const decoded = jwt.verify(token, config.jwt.secret);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// Authorization guard
const authorize = (roles = []) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }
    
    if (roles.length && !roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    
    next();
  };
};

// Rate limiting guard
const rateLimit = (maxRequests = 100, windowMs = 15 * 60 * 1000) => {
  const requests = new Map();
  
  return (req, res, next) => {
    const key = req.ip;
    const now = Date.now();
    const windowStart = now - windowMs;
    
    // Clean old entries
    const userRequests = requests.get(key) || [];
    const validRequests = userRequests.filter(time => time > windowStart);
    
    if (validRequests.length >= maxRequests) {
      return res.status(429).json({
        error: 'Too many requests',
        retryAfter: Math.ceil((validRequests[0] + windowMs - now) / 1000)
      });
    }
    
    validRequests.push(now);
    requests.set(key, validRequests);
    
    next();
  };
};

// Validation guard
const validate = (schema) => {
  return (req, res, next) => {
    const { error } = schema.validate(req.body);
    
    if (error) {
      return res.status(400).json({
        error: 'Validation failed',
        details: error.details.map(detail => detail.message)
      });
    }
    
    next();
  };
};

module.exports = {
  authenticate,
  authorize,
  rateLimit,
  validate
};
javascript
// routes/protected.js - Using guards
const express = require('express');
const { authenticate, authorize, rateLimit } = require('../middleware/auth-guards');
const router = express.Router();

// Apply rate limiting to all routes in this router
router.use(rateLimit(50, 15 * 60 * 1000)); // 50 requests per 15 minutes

// Public route
router.get('/public', (req, res) => {
  res.json({ message: 'This is a public route' });
});

// Authenticated route
router.get('/private', authenticate, (req, res) => {
  res.json({
    message: 'This is a private route',
    user: req.user
  });
});

// Admin only route
router.get('/admin', authenticate, authorize(['admin']), (req, res) => {
  res.json({
    message: 'This is an admin-only route',
    user: req.user
  });
});

// Multiple roles
router.get('/moderator', authenticate, authorize(['admin', 'moderator']), (req, res) => {
  res.json({
    message: 'This route is for admins and moderators',
    user: req.user
  });
});

module.exports = router;

Next Steps

In the next chapter, we'll explore state management in Node.js applications, including session management, caching strategies, and data persistence.

Practice Exercises

  1. Create a RESTful API with full CRUD operations using Express routers
  2. Implement API versioning with backward compatibility
  3. Build a route guard system with role-based access control
  4. Create a dynamic route loading system that supports hot reloading

Key Takeaways

  • Routing determines how applications respond to client requests
  • Express.js provides powerful routing capabilities with middleware support
  • Modular routing improves code organization and maintainability
  • Route guards enable authentication and authorization
  • API versioning allows for backward compatibility
  • Dynamic route loading enables flexible application architecture
  • Proper error handling in routes improves user experience

Content is for learning and research only.