The Philosophy of Logging in NestJS
"Logs are how you talk to your system when you're not there."
The Real Problem With Logs
Have you ever stared at a screen at 3am, production on fire, logs full of entries but nothing useful?
That's not a missing log problem. That's a wrong log problem.
Most developers handle logging on instinct: add it wherever it feels right, format it however, pick a level at random. Nobody thinks about who will read it. The result is thousands of lines of text that can't be queried, can't be traced, can't be understood.
Logging is not a side feature. It is the nervous system of your application, and if you design it badly, you'll find out at the worst possible moment.
Three Questions Before Writing Any Log Line
Who is this log for? Developers need detail to trace bugs. Operators need signals to know if the system is healthy. Monitoring systems need structure to trigger alerts. Three audiences, three different needs. Don't write logs only for yourself.
What question does this log answer? Every log entry should answer a specific question. If you can't picture what question it answers, that line doesn't need to exist.
Where will this log be read? A local terminal needs to look good to the human eye. A production cluster needs JSON for machines to parse. One format can't serve both. Let the environment decide.
Five Principles You Can't Skip
1. Structured, not plain text.
Nobody reads logs when the system is handling thousands of requests per second. People query. Filter. Aggregate. Plain text logs are a relic from when servers processed one request at a time.
Make every entry a JSON object. Fields like event, userId, duration, and requestId are data for machines to process, not sentences for humans to read. Because the question "how many failed login attempts came from the same IP in the last 5 minutes?" can only be answered when the data has structure.
2. Correlation ID throughout.
In a concurrent system, logs from dozens of requests interleave with each other. There's no way to group them. You're reading chaos.
The solution is a single unique ID generated when a request comes in, carried from Middleware through Controller, Service, Database, all the way to Response. Every log line for that request carries the same ID. When you need to trace something, filter by that ID and the entire journey is right there.
In NestJS, use AsyncLocalStorage or nestjs-cls. Context flows automatically without manually injecting it into every service.
3. Log levels are a contract, not a suggestion.
| Level | What it actually means |
|---|---|
error | The system is broken and needs attention now |
warn | Something is off but nothing has failed yet |
info | An important business event just happened |
debug | Technical detail, disabled in production |
When you use error for everything, the ops team stops trusting the alert system. And when everything is an emergency, nothing is.
4. Enough context to make a decision without opening anything else.
"Database query failed" is useless. You need to know: which query, how long before timeout, which request triggered it, which service was handling it. All in one entry. Because when an incident happens, every second spent hunting for more context is another second the system stays broken.
5. Security is a default, not an afterthought.
Passwords, tokens, secret keys, personal data never go into logs. Not "try to avoid it." Never. Build a sanitize() layer from the start that runs automatically on every entry before it's written. Don't rely on remembering to apply it case by case.
Four-Layer Architecture in NestJS
Logs shouldn't be scattered randomly across the codebase. They need layers, each with a clear responsibility.
HTTP Layer (Interceptor) is the entry point. Record every incoming request and outgoing response: method, path, status code, processing time. This is also where the Correlation ID is created before the request goes anywhere.
Business Layer (Service) is where meaningful events live. Order created. Payment processed. User registered. Not every function call, only the things you'll need proof of later, whether they happened or didn't.
Error Layer (ExceptionFilter) is the last safety net. Catch all exceptions, log the full stack trace. System errors (5xx) need an immediate alert, user errors (4xx) only need a warn.
Infrastructure Layer is the most overlooked, but essential. Slow queries, external API timeouts, high cache miss rates. Not business logic, but when production is slow and you don't know why, this is the first place to look.
What Not to Log
Knowing what not to log matters just as much as knowing what to log. Noise buries signal.
"Function getUserById called" has no value. If you need to trace execution flow, use APM. Logging every item in a 10,000-element loop is pure noise. console.log("here 1") left in from debugging doesn't belong in production. And a user typing the wrong password is a warn, not an error. The server losing its database connection is an error.
Logs as a Product
This is the mindset shift that makes the biggest difference, and the one most people skip.
Most developers write logs like they write comments: added for the sake of it, with no thought for the reader. But good logs need to be designed. The same type of event should have the same structure no matter which module handles it. Important logs should be tested like code. The format needs to evolve without breaking downstream consumers. And the team needs someone who owns log quality, not just lets it drift.
When you start treating logs as a product with real users, everything changes.
Closing
Which library, which format, which transport are all details. They can change anytime.
What doesn't change is the philosophy: log to serve, log with purpose, and log like someone who respects the person coming after, whether that's a teammate, the ops team, or yourself three months from now having forgotten everything.
