architecture2025-05-05·8·88/348

Express.js 라우트 미들웨어 순서 문제

Understanding how middleware execution order affects request processing in Express.js and debugging order-related issues.

Express.js 라우트 미들웨어 순서 문제

Introduction

Express.js processes middleware in the exact order they are defined. A misplaced middleware can cause authentication bypasses, missing request data, and silent failures. This post covers how middleware ordering works and how to debug order-related issues.

Environment

node --version
# v20.11.0

npm list express
# my-app@1.0.0
# +-- express@4.18.2

Problem

An authentication middleware was being skipped for certain routes:

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

// Route defined BEFORE middleware
app.get('/admin', (req, res) => {
  // This route is accessible without authentication!
  res.json({ secret: 'admin data' });
});

// Authentication middleware
app.use((req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});

// Other routes protected by middleware
app.get('/dashboard', (req, res) => {
  res.json({ data: 'protected' });
});

The /admin route was accessible without any authentication because it was defined before the auth middleware.

Analysis

Express middleware execution follows a strict top-to-bottom order:

Request → middleware1 → middleware2 → route handler → response

If a route is defined before the middleware, it will never pass through that middleware. The execution model is:

app.use(auth)      → Applied to ALL routes defined AFTER this line
app.get('/a')      → Protected by auth
app.get('/b')      → Protected by auth
app.use(auth2)     → Applied to routes AFTER this line
app.get('/c')      → Protected by auth AND auth2

The key insight is that app.use() and app.get() are executed in the order they appear in the code, and each middleware decides whether to call next() to continue the chain.

Solution

Solution 1: Define middleware before routes

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

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

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

// 3. Authentication middleware
app.use((req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});

// 4. Routes (all protected)
app.get('/admin', (req, res) => {
  res.json({ secret: 'admin data' });
});

app.get('/dashboard', (req, res) => {
  res.json({ data: 'protected' });
});

Solution 2: Route-specific middleware

function authMiddleware(req, res, next) {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
}

// Apply middleware only to specific routes
app.get('/admin', authMiddleware, (req, res) => {
  res.json({ secret: 'admin data' });
});

app.get('/dashboard', authMiddleware, (req, res) => {
  res.json({ data: 'protected' });
});

// Public route - no middleware
app.get('/login', (req, res) => {
  res.json({ message: 'Login page' });
});

Solution 3: Middleware router pattern

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

// Public routes
const publicRouter = express.Router();
publicRouter.get('/login', (req, res) => res.json({ message: 'Login' }));
publicRouter.post('/register', (req, res) => res.json({ message: 'Register' }));

// Protected routes
const protectedRouter = express.Router();
protectedRouter.use((req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});

protectedRouter.get('/admin', (req, res) => res.json({ secret: 'data' }));
protectedRouter.get('/dashboard', (req, res) => res.json({ data: 'protected' }));

app.use('/api', publicRouter);
app.use('/api', protectedRouter);

Solution 4: Debug middleware order

// Debug tool to trace middleware execution
function debugMiddleware(name) {
  return (req, res, next) => {
    console.log(`[${name}] Executing for ${req.method} ${req.url}`);
    next();
  };
}

// Add to each middleware
app.use(debugMiddleware('json-parser'), express.json());
app.use(debugMiddleware('auth'), authMiddleware);
app.use(debugMiddleware('logger'), loggerMiddleware);

app.get('/test', debugMiddleware('route-handler'), (req, res) => {
  res.json({ message: 'Hello' });
});

Solution 5: Conditional middleware

// Skip middleware for certain routes
function conditionalMiddleware(condition, middleware) {
  return (req, res, next) => {
    if (condition(req)) {
      return next();
    }
    middleware(req, res, next);
  };
}

// Skip auth for public paths
const isPublicPath = (req) => 
  req.path.startsWith('/login') || 
  req.path.startsWith('/register') ||
  req.path.startsWith('/health');

app.use(conditionalMiddleware(isPublicPath, authMiddleware));

Lessons Learned

  1. Always define middleware before routes - This is the most common ordering mistake
  2. Use route-specific middleware when different routes need different middleware stacks
  3. Create router modules for complex applications with many middleware layers
  4. Add debug logging to trace middleware execution order
  5. Test middleware order with integration tests that verify authentication and authorization

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.