Hey folks, if you’re running an Express-based shop and still using those basic console.log statements for your app logs, you’re probably dealing with a mess right now. I get it—when you’re cranking out features and trying to hit deadlines, reaching for console.log feels like the fastest move. But let’s be real: when your production app starts acting weird at 2 a.m., scrolling through thousands of garbled text lines (with no timestamps, no context, and no way to filter errors from regular requests) is the worst. That’s where Winston comes in. As an Express solutions provider that’s built, scaled, and troubleshooted 50+ Express apps for clients, I swear by Winston for logging. Today I’m breaking down exactly how to set it up, tweak it to fit your workflow, and stop pulling your hair over unhelpful logs. Express

First Things First: Why Ditch Console.log for Winston?
Wait, hold up—before we jump into code, let’s talk about why Winston’s worth the tiny bit of setup it takes. Console.log is like leaving sticky notes on your office wall to track work; sure, it works for quick tasks, but once you hit 10,000 notes, you can’t find anything. Winston is your organized filing system for logs. Let’s list the real wins (pun intended) that matter for Express devs:
- Proper log levels: Winston lets you separate
error,warn,info,http, anddebuglogs. So you don’t have to guess if a random line is a critical crash or just a normal GET request. - Structured logging (JSON, mostly): Your logs are machine-readable, so you can plug them into tools like Datadog or Elasticsearch later without parsing garbage text.
- Multiple outputs: You can send logs to the console for local dev, a file for production debugging, or even external services like Sentry without writing custom code every time.
- Timestamps and context by default: No more adding
new Date()to every log line manually. Winston will attach timestamps, request IDs, or even user IDs automatically if you set it up right.
For our Express clients, we switched from console.log to Winston on average 2 years ago, and our post-launch troubleshooting time dropped by 40%—no exaggeration.
Step 1: Install the Good Stuff
Alright, let’s get this set up. This is for Express 4.x, since that’s still the most widely used version (most of our clients haven’t jumped to 5.x yet without a good reason). First, open your terminal in your Express project folder. You only need two main packages: winston itself, and the Express middleware that ties it all together, called express-winston. That middleware lets you log every incoming request, response, and even errors right in your Express pipeline.
Run this npm install command—simple, no gotchas here:
npm install winston express-winston
Wait, quick pro tip: if you’re using TypeScript, add @types/winston and @types/express-winston too, but 90% of our clients still use plain JS, so we’ll stick to that today.
Step 2: Set Up the Winston Logger Basics
Now let’s make a logger file. We don’t want to bloat our app.js or index.js with logger code, so create a new folder called utils (most Express projects have this) and add a file called logger.js. Here’s where we configure Winston to work for us, not against us.
Let’s start with the core config. We’ll use two “transports” (that’s Winston lingo for where logs go): the console for local dev, and a daily rotating file for production. Why daily? Because if you log every request to a single file, after a month it’ll be 10GB+ and impossible to parse. The winston-daily-rotate-file package handles that, so install that too:
npm install winston-daily-rotate-file
Now, open utils/logger.js and paste this. I’ll explain each part so you don’t just copy-paste blind:
const { createLogger, format, transports } = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const { combine, timestamp, printf, json } = format;
// Custom log format for console (easier to read locally)
const consoleFormat = printf(({ level, message, timestamp, ...meta }) => {
return `${timestamp} [${level.toUpperCase()}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}`;
});
// Build the actual logger
const logger = createLogger({
// Log levels—we'll use the standard npm levels here
level: process.env.NODE_ENV === 'production' ? 'http' : 'debug',
format: combine(timestamp(), json()), // JSON for files, structured for console
transports: [
// Console transport for local dev
new transports.Console({ format: combine(timestamp({ format: 'HH:mm:ss' }), consoleFormat) }),
// Daily rotating file for production logs
new DailyRotateFile({
filename: 'logs/app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m', // Split files at 20MB so they're manageable
maxFiles: '14d', // Delete logs older than 14 days to save space
level: 'http' // Only log http and above to the file (no debug noise)
})
],
// Don't exit on uncaught exceptions—let Express handle that
exitOnError: false
});
module.exports = logger;
Wait, why the different formats? Local dev doesn’t need giant JSON blobs you have to parse in your head—so we use a human-friendly printf format for the console. Production needs JSON so your monitoring tool can pick it up, hence the json() format. And the log level thing: in local dev, we log everything (debug to error), but in production, we skip super noisy debug logs and only keep http (all requests) and higher, which is exactly what we need.
Also, that DailyRotateFile config is non-negotiable for production. I’ve seen clients use a single log file that got so big, their server’s disk filled up and took down the whole app. Don’t be that guy.
Step 3: Integrate with Express (The Important Part)
Okay, now we have a logger, but how do we make it log every Express request and error? That’s where express-winston comes in. We don’t want to manually add logger.info('Incoming request') to every route—we want Express to handle it automatically.
First, head to your main Express file (usually app.js or index.js). You’ll need to require two things: your custom logger, and express-winston. Here’s what that part looks like:
const express = require('express');
const logger = require('./utils/logger');
const { expressWinston } = require('express-winston');
const app = express();
// Add the request logger middleware *BEFORE* your routes
app.use(expressWinston.logger({
winstonInstance: logger, // Use our custom Winston setup
meta: true, // Attach extra metadata (like request IP, user agent)
msg: 'HTTP {{req.method}} {{req.url}}', // Custom message for requests
expressFormat: true, // Use Express's default request format for clarity
colorize: process.env.NODE_ENV === 'development' // Color logs in local console
}));
// Then all your actual routes go here
app.get('/', (req, res) => res.send('Hello World!'));
app.get('/api/users/:id', (req, res) => res.json({ id: req.params.id, name: 'Jake' }));
// Add the error logger middleware *AFTER* your routes (and any error-handling middleware)
app.use(expressWinston.errorLogger({
winstonInstance: logger
}));
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => logger.info(`Server running on port ${PORT}`));
Wait a second—order matters here. The request logger has to go before your routes, otherwise it won’t log requests. The error logger has to go after routes, so it can catch any errors that come up in your route handlers. That’s a tiny detail that trips up so many devs when they first set this up. I’ve seen at least 3 client projects where the error logger was in the wrong spot, so critical errors were just vanishing into thin air. Fix that, and you’re already ahead of the game.
Step 4: Add Custom Logs for Your App-Specific Stuff
Okay, so now you’ve got all Express requests and errors logged, but what about stuff unique to your app? Like if your user updates their profile, or a payment fails? You don’t want those buried in the http logs. That’s where you use the logger instance directly in your code.
Let’s take a simple example: a user signup route. Here’s how you’d add a custom log for when a new user is created:
app.post('/api/auth/signup', async (req, res) => {
try {
const newUser = await User.create(req.body);
// Log a custom info-level event with the user's ID and email
logger.info('New user signed up', { userId: newUser.id, email: newUser.email, event: 'user_signup' });
res.status(201).json(newUser);
} catch (err) {
// Log the error with extra context so you don't just see "Database error"
logger.error('User signup failed', { email: req.body.email, error: err.message, stack: err.stack });
res.status(500).json({ message: 'Signup failed, try again later' });
}
});
See the context we’re adding? Instead of just logging err, we’re logging the email that tried to sign up, the error message, and even the stack trace. When we get a support ticket that says “my signup didn’t work”, we can check the logs and immediately see if it’s a database issue, a duplicate email, or something else. That’s the kind of detail that cuts support time in half.
Pro Tips to Tweak for Production (We’ve Tested This)
Now that you’ve got the basics down, let’s add a few tweaks that we’ve battle-tested on our client apps, because one size doesn’t fit all.
First: Hide sensitive data. You don’t want passwords, credit card numbers, or API keys showing up in your logs. We use the blacklist option in express-winston to strip those out. For example, add blacklist: ['password', 'creditCard', 'apiKey'] to both the expressWinston.logger and errorLogger configs. That’s non-negotiable—GDPR and PCI will come for you if you log sensitive data.
Second: Adjust log levels for production. Wait, we mentioned this earlier, but double-check: in production, set level: 'http' for the file transport. If you set it to debug, you’ll end up with so many logs that your file storage fills up in days. For most Express apps, http (all requests) and error are more than enough.
Third: Add request IDs. When you’re troubleshooting a specific user’s request, being able to search logs for their request ID is a game-changer. You can generate a unique ID on every incoming request and add it to all logs for that request. Here’s a quick way to do it with uuid:
Install it: npm install uuid
Add this middleware before your request logger:
const { v4: uuidv4 } = require('uuid');
app.use((req, res, next) => {
req.id = uuidv4();
next();
});
Then update your custom logs to include { requestId: req.id }, and express-winston will automatically pick up the req.id and add it to all request/error logs. So you can search logs by requestId and see exactly what happened to that specific request, no scrolling needed.
Common Mistakes We See (Avoid These)
Let’s wrap this up with the mistakes we’ve seen devs make when setting up Winston for Express, because you don’t have to learn them the hard way.
- Forgetting transport levels: We had a client once that logged
debugto their production file, and their server’s disk got full in 3 days. Don’t do that—match transport levels to your environment. - Putting the error logger in the wrong spot: Like I said earlier, error logger has to go after routes. We fixed a client issue last month where they had it before routes, so all errors were being caught by their 404 handler instead of the error logger. Total chaos.
- Not blacklisting sensitive data: Another client had a bug where they logged the
authorizationheader, so we saw hundreds of API keys in their logs within hours of a tiny deployment. They had to rotate every key, which cost them $1,500 in downtime and dev time. Don’t skip the blacklist. - Overcomplicating the format: You don’t need 10 different custom formats. Start simple, use JSON for production, human-readable for local. Add custom context only when you need it.
That’s pretty much it! Winston isn’t the fanciest logging tool out there, but for Express apps, it’s the sweet spot between simplicity and power. We still use it for every single Express project we build, and every time a client comes to us with a logging headache from their old console.log setup, the first thing we do is switch them over to Winston.

If you’re an Express dev or a business that runs Express apps and you’re tired of vague logs, late-night troubleshooting sessions, or production issues you can’t track down, we can help. Whether you just need help setting up Winston for your existing app, or you want to build a new Express app with rock-solid logging from day one, our team has the expertise to get it right without the growing pains. Reach out to our team to connect and chat through your specific needs—we’re here to make logging (and your Express apps) way less painful.
References
Dropshipping Alibaba Node.js Winston Official Documentation
Express-Winston GitHub Repository
Winston Daily Rotate File Package Notes
Express 4.x Middleware Order Guidelines
Foshan Shijie International Logistics Co., Ltd.
With abundant experience, we are one of the most professional express service suppliers in China. We are committed to offering reliable express service and logistics solutions with low price. If you have any enquiry about quote, please feel free to email us.
Address: Room 701A, Office Building, Jinbo Commercial Center, No. 88, Guiye Road, Guicheng Street, Nanhai District, Foshan City, Guangdong Province
E-mail: alicesmith.tradingbusiness@gmail.com
WebSite: https://www.fsshijielogistics.com/