Exercises
Objectives
By the end of this lesson, you should be able to:
- Combine request logging, operational-error logging, and rate-limit logging on real auth routes
- Distinguish, from log output alone, a validation failure, a bad login, and a blocked attack
⚠️ A note on verification: every command and every output in this lesson was actually run.
Exercise: Logging a Complete Auth Flow
a) Build the app, register and login, both logged, plus a rate limiter with its own logging:
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 3,
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
req.log.warn({ status: 429 }, 'Rate limit exceeded on login');
res.status(429).json({ error: 'TooManyRequests', message: 'Too many login attempts, try again later' });
}
});
app.post('/api/v1/auth/register', async (req, res, next) => {
const { email, password } = req.body;
if (!email || !password) {
return next(new AppError('email and password are required', 400));
}
const passwordHash = await bcrypt.hash(password, 10);
users.push({ id: users.length + 1, email, passwordHash });
req.log.info({ userId: users.length }, 'User registered');
res.status(201).json({ email });
});
app.post('/api/v1/auth/login', loginLimiter, async (req, res, next) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return next(new AppError('Incorrect email or password', 401));
}
req.log.info({ userId: user.id }, 'User logged in');
res.json({ success: true });
});
rateLimit’s handler option runs instead of the default response when a client is blocked, used here to log the block explicitly, at warn, exactly like any other operational error, being rate-limited is expected, routine behavior for the system, not a bug.
b) Run a sequence: register successfully, register with a missing password, then send four wrong-password login attempts in a row (max: 3 blocks the fourth):
User registered level: info (30)
Operational error handled status:400 "email and password are required" level: warn (40)
Operational error handled status:401 "Incorrect email or password" level: warn (40)
Operational error handled status:401 "Incorrect email or password" level: warn (40)
Operational error handled status:401 "Incorrect email or password" level: warn (40)
Rate limit exceeded on login status:429 level: warn (40)
Every line here is info or warn, nothing hits error, correctly, nothing in this sequence is an actual bug, a missing field, a wrong password, and a blocked attack are all expected, handled outcomes.
c) Confirm what error-level logging would look like. Trigger a genuinely unexpected error (an unmarked Error, not an AppError, thrown from inside a route), and confirm it logs at error (level: 50), with the full error object attached, distinct from every line in the sequence above.
d) Reflect. A team monitoring this application would reasonably configure alerts to fire only on error-level logs. Looking at the six log lines above, explain, in one or two sentences, why none of them should have paged anyone, even the rate-limited attack attempt.
Recap
This module replaced console.log with structured, leveled logging: Pino’s JSON output, pino-http tying logs to individual requests, and a consistent operational-versus-programmer distinction carried all the way from Node.js & Express Foundations into what gets logged at warn versus error. Combined with Module 7 and 8’s automated tests, this course’s API now has both correctness checks and production visibility, not just code that happens to work when run by hand.
Next module: the capstone, bringing every module in this course, hashing, sessions or JWTs, roles, validation, hardening, testing, and logging, together into one complete, secured, tested API.