Episode 3 — NodeJS MongoDB Backend Architecture / 3.6 — Middleware in Express

3.6.c — Application-Level Middleware

In one sentence: Application-level middleware is registered directly on the Express app instance using app.use() or app.METHOD(), and it can run globally for every request or be scoped to specific path prefixes -- making it the primary way to set up cross-cutting concerns like logging, authentication, and body parsing.

Navigation: <- 3.6.b Types of Middleware | 3.6.d -- Router-Level Middleware ->


1. app.use(middleware) -- Global Middleware

When you call app.use() with only a middleware function (no path), it runs for every incoming request, regardless of the HTTP method or URL.

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

// Runs for EVERY request: GET, POST, PUT, DELETE, PATCH, etc.
app.use((req, res, next) => {
  console.log(`[Global] ${req.method} ${req.url}`);
  next();
});

app.get('/', (req, res) => res.send('Home'));
app.get('/about', (req, res) => res.send('About'));
app.post('/data', (req, res) => res.send('Data received'));

app.listen(3000);

All three routes see the log middleware run first:

[Global] GET /
[Global] GET /about
[Global] POST /data

2. app.use('/path', middleware) -- Path-Scoped Middleware

Add a path prefix as the first argument to limit when the middleware runs.

// Only runs for requests starting with /api
app.use('/api', (req, res, next) => {
  console.log('[API] Request to API route');
  next();
});

// Only runs for requests starting with /admin
app.use('/admin', (req, res, next) => {
  console.log('[Admin] Checking admin access');
  next();
});

app.get('/', (req, res) => res.send('Home'));              // No middleware
app.get('/api/users', (req, res) => res.send('Users'));    // API middleware runs
app.get('/api/products', (req, res) => res.send('Prods')); // API middleware runs
app.get('/admin/dashboard', (req, res) => res.send('Dash')); // Admin middleware runs

Path matching rules:

Path PrefixMatchesDoes NOT Match
'/'Everything(matches all)
'/api'/api, /api/users, /api/users/123/application, /about
'/api/users'/api/users, /api/users/123/api/products

Important: The path prefix is a starts-with match, not an exact match. /api matches /api/anything.


3. Order of app.use() Matters

Express processes middleware in registration order. The first app.use() in your code runs first.

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

app.use((req, res, next) => {
  console.log('1. First middleware');
  next();
});

app.use((req, res, next) => {
  console.log('2. Second middleware');
  next();
});

app.use((req, res, next) => {
  console.log('3. Third middleware');
  next();
});

app.get('/', (req, res) => {
  console.log('4. Route handler');
  res.send('Done');
});

app.listen(3000);

Output for GET /:

1. First middleware
2. Second middleware
3. Third middleware
4. Route handler

Order gone wrong -- a common bug

// BUG: Auth middleware BEFORE body parser
app.use('/api', (req, res, next) => {
  // Trying to read the body for token...
  const token = req.body.token; // undefined! Body hasn't been parsed yet
  if (!token) return res.status(401).json({ error: 'No token' });
  next();
});

app.use(express.json()); // Too late for the auth middleware above!

app.post('/api/data', (req, res) => {
  res.json({ data: req.body });
});

Fix: Always put body parsers before anything that reads req.body:

app.use(express.json());  // 1. Parse body FIRST

app.use('/api', (req, res, next) => {
  const token = req.body.token; // Now available
  if (!token) return res.status(401).json({ error: 'No token' });
  next();
});

app.post('/api/data', (req, res) => {
  res.json({ data: req.body });
});

4. Multiple Middleware in One app.use()

You can pass multiple middleware functions in a single app.use() call. They execute left-to-right.

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

const addTimestamp = (req, res, next) => {
  req.timestamp = new Date().toISOString();
  next();
};

const addRequestId = (req, res, next) => {
  req.id = Math.random().toString(36).slice(2, 10);
  next();
};

// All three run in order for every request
app.use(logRequest, addTimestamp, addRequestId);

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

You can also pass an array:

const commonMiddleware = [logRequest, addTimestamp, addRequestId];
app.use(commonMiddleware);

5. app.use() vs app.METHOD()

SyntaxWhen It Runs
app.use(mw)All HTTP methods (GET, POST, PUT, DELETE, PATCH, etc.)
app.get('/path', mw)Only GET requests to /path
app.post('/path', mw)Only POST requests to /path
app.use('/path', mw)All methods for URLs starting with /path
// Runs for ALL methods on ALL paths
app.use(express.json());

// Runs only for GET /health
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// Runs for ALL methods on /api/*
app.use('/api', corsMiddleware);

// Runs only for POST /api/users
app.post('/api/users', createUser);

6. Global Middleware Patterns

6.1 Request Timestamp Logger

app.use((req, res, next) => {
  const now = new Date();
  req.requestTime = now.toISOString();

  console.log(`[${req.requestTime}] ${req.method} ${req.url} from ${req.ip}`);
  next();
});

app.get('/status', (req, res) => {
  res.json({
    status: 'ok',
    requestedAt: req.requestTime
  });
});

6.2 API Key Checker

const requireApiKey = (req, res, next) => {
  const key = req.headers['x-api-key'] || req.query.api_key;

  if (!key) {
    return res.status(401).json({
      error: 'API key is required',
      hint: 'Send it via x-api-key header or ?api_key= query parameter'
    });
  }

  if (key !== process.env.API_KEY) {
    return res.status(403).json({ error: 'Invalid API key' });
  }

  next();
};

// Only API routes need the key
app.use('/api', requireApiKey);

// Public routes (no API key needed)
app.get('/', (req, res) => res.send('Welcome'));
app.get('/health', (req, res) => res.json({ status: 'ok' }));

// Protected routes
app.get('/api/users', (req, res) => res.json({ users: [] }));
app.get('/api/products', (req, res) => res.json({ products: [] }));

6.3 Request ID Generator

const crypto = require('crypto');

app.use((req, res, next) => {
  // Use existing ID from load balancer or generate new one
  req.id = req.headers['x-request-id'] || crypto.randomUUID();

  // Add to response headers for tracing
  res.setHeader('X-Request-ID', req.id);

  next();
});

app.get('/api/data', (req, res) => {
  res.json({ data: 'hello', requestId: req.id });
});
// Response includes header: X-Request-ID: a1b2c3d4-...

7. Conditional Middleware Based on Environment

Different middleware for development vs production.

const express = require('express');
const morgan = require('morgan');
const compression = require('compression');
const helmet = require('helmet');

const app = express();

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

// --- Development only ---
if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));                    // Verbose colored logs
  console.log('Development middleware loaded');
}

// --- Production only ---
if (process.env.NODE_ENV === 'production') {
  app.use(helmet());                         // Security headers
  app.use(compression());                    // Gzip compression
  app.use(morgan('combined'));               // Apache-style logs

  console.log('Production middleware loaded');
}

// --- Debug mode ---
if (process.env.DEBUG_MODE === 'true') {
  app.use((req, res, next) => {
    console.log('Headers:', req.headers);
    console.log('Body:', req.body);
    console.log('Query:', req.query);
    next();
  });
}

app.get('/', (req, res) => res.send('Hello'));
app.listen(3000);

Run with environment:

NODE_ENV=development node server.js
NODE_ENV=production node server.js
DEBUG_MODE=true node server.js

8. Middleware That Modifies the Response

Middleware can also hook into the response lifecycle.

// Add server timing header to all responses
app.use((req, res, next) => {
  const start = process.hrtime();

  // Hook into the 'finish' event (fires when response is sent)
  res.on('finish', () => {
    const diff = process.hrtime(start);
    const timeMs = (diff[0] * 1e3 + diff[1] / 1e6).toFixed(2);
    console.log(`${req.method} ${req.url} -- ${res.statusCode} -- ${timeMs}ms`);
  });

  next();
});

// Add custom headers to all responses
app.use((req, res, next) => {
  res.setHeader('X-Powered-By', 'My Express App');
  res.setHeader('X-API-Version', '1.0.0');
  next();
});

9. Skipping Middleware for Certain Routes

Sometimes you want middleware to run for all routes except specific ones.

const authMiddleware = (req, res, next) => {
  // Skip auth for public routes
  const publicPaths = ['/health', '/login', '/register', '/docs'];

  if (publicPaths.includes(req.path)) {
    return next(); // Skip authentication
  }

  // Check token
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  // Verify token (simplified)
  try {
    req.user = verifyToken(token); // Your token verification logic
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

app.use(authMiddleware);

Alternative pattern -- apply middleware only to specific routes:

// Instead of skip-logic, only apply where needed:
app.get('/health', (req, res) => res.json({ status: 'ok' })); // No auth
app.post('/login', loginHandler);                                // No auth

app.use('/api', authMiddleware); // Auth only for /api/*
app.get('/api/profile', getProfile);
app.get('/api/settings', getSettings);

10. Execution Visualization

Request: POST /api/users   Body: { "name": "Alice" }

  app.use(express.json())         --> Parses body, sets req.body
       |
       v
  app.use(morgan('dev'))          --> Logs: POST /api/users
       |
       v
  app.use('/api', requireApiKey)  --> Checks x-api-key header
       |                               |
       | (key valid)                    | (key missing/invalid)
       v                               v
  app.post('/api/users', handler) --> 401 { error: "..." }
       |
       v
  res.status(201).json(user)      --> Response sent

11. Complete Working Example

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

// ---- Global Middleware (runs for everything) ----

// 1. Parse request bodies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 2. Log every request
app.use((req, res, next) => {
  req.startTime = Date.now();
  console.log(`--> ${req.method} ${req.url}`);
  next();
});

// 3. Add request ID
app.use((req, res, next) => {
  req.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
  res.setHeader('X-Request-ID', req.id);
  next();
});

// ---- Path-Scoped Middleware ----

// 4. API key required for /api routes
app.use('/api', (req, res, next) => {
  const key = req.headers['x-api-key'];
  if (key !== 'my-secret-key') {
    return res.status(401).json({ error: 'Invalid API key' });
  }
  next();
});

// 5. Admin check for /admin routes
app.use('/admin', (req, res, next) => {
  const role = req.headers['x-user-role'];
  if (role !== 'admin') {
    return res.status(403).json({ error: 'Admin access only' });
  }
  next();
});

// ---- Routes ----

// Public routes (only global middleware runs)
app.get('/', (req, res) => {
  res.json({ message: 'Welcome!', requestId: req.id });
});

app.get('/health', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

// API routes (global + API key middleware)
app.get('/api/users', (req, res) => {
  res.json({ users: [{ id: 1, name: 'Alice' }] });
});

app.post('/api/users', (req, res) => {
  res.status(201).json({ created: req.body });
});

// Admin routes (global + admin middleware)
app.get('/admin/dashboard', (req, res) => {
  res.json({ stats: { users: 100, orders: 50 } });
});

// ---- After routes ----

// 404 handler
app.use((req, res) => {
  res.status(404).json({
    error: 'Not found',
    path: req.url,
    requestId: req.id
  });
});

// Error handler
app.use((err, req, res, next) => {
  const duration = Date.now() - req.startTime;
  console.error(`[ERROR] ${req.method} ${req.url} (${duration}ms):`, err.message);
  res.status(500).json({ error: 'Internal server error', requestId: req.id });
});

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

Key Takeaways

  1. app.use(mw) registers middleware for all methods and paths -- true global middleware.
  2. app.use('/path', mw) scopes middleware to a path prefix -- it runs only when the URL starts with that path.
  3. Registration order = execution order. Body parsers must come before anything that reads req.body.
  4. Multiple middleware can be passed in one app.use() call or as an array.
  5. Use environment checks (NODE_ENV) to load different middleware in development vs production.
  6. Path-scoped middleware is the primary way to separate public vs protected routes at the application level.

Explain-It Challenge

Explain without notes:

  1. What is the difference between app.use(mw) and app.use('/api', mw)?
  2. Why must express.json() be registered before route handlers that read req.body?
  3. How would you apply authentication middleware to all /api/* routes but skip /health and /login?
  4. What happens if you register a 404 handler before your routes?

Navigation: <- 3.6.b Types of Middleware | 3.6.d -- Router-Level Middleware ->