Skip to main content
โšก Calmops

Routing and Middleware in Express

Routing and Middleware in Express

Advanced routing and middleware patterns are essential for building scalable Express applications. This article covers advanced techniques.

Introduction

Advanced routing and middleware provide:

  • Organized code structure
  • Reusable middleware
  • Complex routing patterns
  • Request processing pipelines
  • Clean architecture

Understanding advanced patterns helps you:

  • Build maintainable applications
  • Implement authentication
  • Handle cross-cutting concerns
  • Create flexible APIs
  • Scale applications

Advanced Routing

Route Parameters and Patterns

const express = require('express');
const app = express();

// โœ… Good: Route parameters
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.json({ id: userId });
});

// โœ… Good: Multiple parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.json({ userId, postId });
});

// โœ… Good: Optional parameters
app.get('/files/:name?', (req, res) => {
  const fileName = req.params.name || 'default';
  res.json({ file: fileName });
});

// โœ… Good: Regex patterns
app.get('/users/:id(\\d+)', (req, res) => {
  const userId = req.params.id;
  res.json({ id: userId });
});

// โœ… Good: Query parameters
app.get('/search', (req, res) => {
  const { q, page = 1, limit = 10 } = req.query;
  res.json({ query: q, page, limit });
});

// โœ… Good: Wildcard routes
app.get('/api/*', (req, res) => {
  res.json({ path: req.params[0] });
});

// โœ… Good: Route chaining
app.route('/users/:id')
  .get((req, res) => {
    res.json({ id: req.params.id });
  })
  .put((req, res) => {
    res.json({ updated: req.params.id });
  })
  .delete((req, res) => {
    res.json({ deleted: req.params.id });
  });

Router Objects

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

// โœ… Good: Router middleware
router.use((req, res, next) => {
  console.log('User route accessed');
  next();
});

// โœ… Good: Router parameters
router.param('id', (req, res, next, id) => {
  console.log('User ID:', id);
  req.userId = id;
  next();
});

// โœ… Good: Router routes
router.get('/', (req, res) => {
  res.json([{ id: 1, name: 'John' }]);
});

router.get('/:id', (req, res) => {
  res.json({ id: req.userId, name: 'John' });
});

router.post('/', (req, res) => {
  res.status(201).json(req.body);
});

module.exports = router;

// routes/posts.js
const express = require('express');
const router = express.Router({ mergeParams: true });

router.get('/', (req, res) => {
  const userId = req.params.userId;
  res.json([{ id: 1, userId, title: 'Post 1' }]);
});

module.exports = router;

// app.js
const express = require('express');
const userRoutes = require('./routes/users');
const postRoutes = require('./routes/posts');

const app = express();

app.use('/users', userRoutes);
app.use('/users/:userId/posts', postRoutes);

app.listen(3000);

Middleware Patterns

Middleware Chains

const express = require('express');
const app = express();

// โœ… Good: Middleware chain
const authenticate = (req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  req.user = { id: 1, name: 'John' };
  next();
};

const authorize = (role) => {
  return (req, res, next) => {
    if (req.user.role !== role) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
};

const validate = (schema) => {
  return (req, res, next) => {
    const { error } = schema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: error.message });
    }
    next();
  };
};

// โœ… Good: Apply middleware chain
app.post(
  '/admin/users',
  authenticate,
  authorize('admin'),
  validate(userSchema),
  (req, res) => {
    res.json({ message: 'User created' });
  }
);

// โœ… Good: Conditional middleware
app.get('/data', (req, res, next) => {
  if (req.query.admin) {
    authenticate(req, res, next);
  } else {
    next();
  }
}, (req, res) => {
  res.json({ data: 'public data' });
});

Custom Middleware

// middleware/logger.js
function logger(req, res, next) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
  });

  next();
}

module.exports = logger;

// middleware/requestId.js
const { v4: uuidv4 } = require('uuid');

function requestId(req, res, next) {
  req.id = uuidv4();
  res.setHeader('X-Request-ID', req.id);
  next();
}

module.exports = requestId;

// middleware/cors.js
function cors(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type');

  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }

  next();
}

module.exports = cors;

// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  console.error('Error:', err);

  const status = err.status || 500;
  const message = err.message || 'Internal server error';

  res.status(status).json({
    error: {
      status,
      message,
      requestId: req.id
    }
  });
}

module.exports = errorHandler;

// app.js
const express = require('express');
const logger = require('./middleware/logger');
const requestId = require('./middleware/requestId');
const cors = require('./middleware/cors');
const errorHandler = require('./middleware/errorHandler');

const app = express();

app.use(logger);
app.use(requestId);
app.use(cors);
app.use(express.json());

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

app.use(errorHandler);

app.listen(3000);

Authentication and Authorization

JWT Authentication

// middleware/auth.js
const jwt = require('jsonwebtoken');

const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

const authorize = (roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
};

module.exports = { authenticate, authorize };

// routes/auth.js
const express = require('express');
const jwt = require('jsonwebtoken');
const router = express.Router();

router.post('/login', (req, res) => {
  const { email, password } = req.body;

  // Verify credentials
  const user = { id: 1, email, role: 'user' };

  const token = jwt.sign(user, process.env.JWT_SECRET, {
    expiresIn: '1h'
  });

  res.json({ token });
});

module.exports = router;

// routes/protected.js
const express = require('express');
const { authenticate, authorize } = require('../middleware/auth');
const router = express.Router();

router.get('/profile', authenticate, (req, res) => {
  res.json({ user: req.user });
});

router.get('/admin', authenticate, authorize(['admin']), (req, res) => {
  res.json({ message: 'Admin panel' });
});

module.exports = router;

Request Validation

Input Validation Middleware

// middleware/validate.js
const { body, validationResult } = require('express-validator');

const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  next();
};

module.exports = validate;

// routes/users.js
const express = require('express');
const { body } = require('express-validator');
const validate = require('../middleware/validate');
const router = express.Router();

router.post(
  '/',
  [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }),
    body('name').trim().notEmpty()
  ],
  validate,
  (req, res) => {
    res.status(201).json(req.body);
  }
);

router.put(
  '/:id',
  [
    body('email').optional().isEmail(),
    body('password').optional().isLength({ min: 8 })
  ],
  validate,
  (req, res) => {
    res.json({ id: req.params.id, ...req.body });
  }
);

module.exports = router;

Error Handling Middleware

Comprehensive Error Handling

// middleware/errorHandler.js
class AppError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
  }
}

const errorHandler = (err, req, res, next) => {
  console.error('Error:', err);

  const status = err.status || 500;
  const message = err.message || 'Internal server error';

  res.status(status).json({
    error: {
      status,
      message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
};

const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

module.exports = { AppError, errorHandler, asyncHandler };

// routes/users.js
const express = require('express');
const { AppError, asyncHandler } = require('../middleware/errorHandler');
const router = express.Router();

router.get('/:id', asyncHandler(async (req, res) => {
  const user = await getUser(req.params.id);
  if (!user) {
    throw new AppError('User not found', 404);
  }
  res.json(user);
}));

module.exports = router;

// app.js
const express = require('express');
const { errorHandler } = require('./middleware/errorHandler');
const app = express();

app.use(express.json());

// Routes
app.use('/users', require('./routes/users'));

// Error handling
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

app.use(errorHandler);

app.listen(3000);

Best Practices

  1. Order middleware correctly:

    // โœ… Good: Correct order
    app.use(express.json());
    app.use(authenticate);
    app.use(routes);
    app.use(errorHandler);
    
    // โŒ Bad: Wrong order
    app.use(errorHandler);
    app.use(routes);
    app.use(authenticate);
    
  2. Use async/await with error handling:

    // โœ… Good: Async handler
    const asyncHandler = (fn) => (req, res, next) => {
      Promise.resolve(fn(req, res, next)).catch(next);
    };
    
    // โŒ Bad: No error handling
    app.get('/', async (req, res) => {
      const data = await fetchData();
      res.json(data);
    });
    
  3. Separate concerns:

    // โœ… Good: Separate middleware
    app.use(authenticate);
    app.use(authorize);
    app.use(validate);
    
    // โŒ Bad: Mixed concerns
    app.use((req, res, next) => {
      // Authentication, authorization, validation all here
    });
    

Summary

Advanced routing and middleware are essential. Key takeaways:

  • Use route parameters and patterns
  • Create reusable middleware
  • Implement authentication and authorization
  • Validate input properly
  • Handle errors comprehensively
  • Organize code by concern
  • Use async/await with error handling
  • Build scalable applications

Next Steps

Comments