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()orapp.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 Prefix | Matches | Does 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()
| Syntax | When 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
app.use(mw)registers middleware for all methods and paths -- true global middleware.app.use('/path', mw)scopes middleware to a path prefix -- it runs only when the URL starts with that path.- Registration order = execution order. Body parsers must come before anything that reads
req.body. - Multiple middleware can be passed in one
app.use()call or as an array. - Use environment checks (
NODE_ENV) to load different middleware in development vs production. - Path-scoped middleware is the primary way to separate public vs protected routes at the application level.
Explain-It Challenge
Explain without notes:
- What is the difference between
app.use(mw)andapp.use('/api', mw)? - Why must
express.json()be registered before route handlers that readreq.body? - How would you apply authentication middleware to all
/api/*routes but skip/healthand/login? - What happens if you register a 404 handler before your routes?
Navigation: <- 3.6.b Types of Middleware | 3.6.d -- Router-Level Middleware ->