Posted in

How to use Morgan for logging in Express?

Hey everyone, if you’re running an Express.js shop like we are, you know how chaotic logging can get when your app starts scaling. Last quarter, our team was digging through a mountain of messy console logs, trying to track down why one of our client’s payment endpoints was crashing randomly at 2 a.m. We missed three bugs before we realized we needed something solid, reliable, and way less clunky than writing custom log functions from scratch. That’s when we found Morgan, and let me tell you—it’s been a game changer for our workflow. If you’re tired of sifting through generic logs that give you zero context, stick around—this post’s gonna walk you through exactly how we use Morgan to keep our Express apps sane. Express

First off, let’s cut the jargon: Morgan is basically a middleware logger for Express. Middleware is just code that runs between when a request hits your server and when it sends a response, right? So Morgan sits in that gap, grabs all the details from every incoming request and outgoing response, and dumps them into clean, readable logs. No more writing console.log(req.method) 100 times per route—Morgan does all the heavy lifting for you. As an Express provider, we’ve tested every logging tool out there, and Morgan hits that sweet spot of being lightweight, flexible, and not adding bloat to your app. It’s not overkill for small projects but scales perfectly for the enterprise clients we work with, who need to track dozens of concurrent requests.

Let’s start with the basics of setting it up—this is super straightforward, so even if you’re new to Express, you’ll get it in two minutes. First, make sure you’ve got an Express project going (if not, just run npm init -y and npm install express like you normally would). Then install Morgan with npm install morgan—that’s it. No extra dependencies, no weird configuration hoops. Once it’s installed, you just require it in your Express app file, right at the top. We always put Morgan right after we initialize Express, because middleware order matters. If you put it after your routes, it won’t log those requests—trust me, we learned that the hard way once and had no logs for a whole day of client traffic. That was not a good week.

Wait, before we dive into the actual code, let’s talk about the predefined formats Morgan gives you, because that’s where most people start. Morgan has a bunch of built-in tokens (like method, status, url, response time) and pre-made format strings that throw those together nicely. The most common ones are dev, combined, common, and short. Let’s break down what each does, because picking the wrong format can make your logs either useless or way too long. The dev format is perfect for local development—it’s colorful, concise, and shows you exactly what you need: HTTP method, route, status code, and response time. When we’re building new features, that’s all we use. We can see at a glance if a GET request is taking too long, or if a POST is throwing a 404. The combined format is way more detailed—it adds IP addresses, referrers, user agents, and even the response size. That’s what we use for production, because we need all that context to debug weird issues, like a bot hitting our API 10,000 times an hour from a specific IP. We don’t use common or short much—short is too bare-bones, and common is missing a lot of the timestamps we rely on.

Let’s actually write that code so you can see how simple this is. If you’ve got a basic Express app, it’ll look something like this:

First, require express and morgan:
const express = require(‘express’);
const morgan = require(‘morgan’);
const app = express();

// Then use Morgan—this is the line people get right away, but we tweak it for dev vs prod
if (process.env.NODE_ENV === ‘production’) {
app.use(morgan(‘combined’));
} else {
app.use(morgan(‘dev’));
}

// Then add your routes like normal
app.get(‘/’, (req, res) => {
res.send(‘Hello from our Express shop!’);
});

app.listen(3000, () => {
console.log(‘Server running on port 3000’);
});

That’s it? Yeah, that’s the entire setup for basic logging. Wait, but let’s be real—most of us don’t stop at basic. If you’re running a business, you need more than just those predefined formats. For example, we wanted timestamps on every log line (the combined format doesn’t always show time in a way that’s easy to parse at a glance), and we also wanted to log the user ID of authenticated users, since we work with apps that have user-specific data. Morgan lets you create custom tokens, which is how we do that. Tokens are just functions that return a value you want to log, so you can mix and match built-in tokens with your own.

Here’s how we set up a custom timestamp token and a user ID token, for example. In your app file, before you use Morgan, add this:
morgan.token(‘timestamp’, () => new Date().toISOString());
morgan.token(‘userId’, (req) => req.user ? req.user.id : ‘guest’);

Then when you initialize Morgan, you pass in a string that uses these tokens. For production, our production format string is: :timestamp :remote-addr :userId :method :url :status :response-time ms - :res[content-length]

What does that do? It gives us a log line that looks like this: 2024-05-20T14:30:00.123Z 192.168.1.45 12345 GET /api/orders 200 45 ms - 1234 That’s perfect for us—we can see exactly when the request happened, who the user is, where it came from, what method was used, which route, the status, how long it took, and the size of the response. No more guessing what a random log entry is, because every part is there. And since we made a userId token, we don’t have to manually pass that around every time we make a request log—Morgan grabs it automatically if the user is logged in, or marks them as a guest if not.

Another thing we love about Morgan is that it lets you stream logs to a file instead of just printing them to the console. For development, console logs are fine, but in production, you don’t want your logs sitting in Heroku’s logs (or whatever host you use) because they get rotated and deleted after a while. We use fs.createWriteStream to stream all production logs to a file, and we even rotate them daily with a tool called winston-daily-rotate-file, but Morgan handles the streaming part super easily. Here’s how we do that—first, require Node’s fs module:
const fs = require(‘fs’);
const path = require(‘path’);

// Create a write stream to a logs folder in our project root, named with the date
const logStream = fs.createWriteStream(path.join(__dirname, ‘logs’, ‘production.log’), { flags: ‘a’ });

// Then in production, pass the stream to Morgan
app.use(morgan(‘combined’, { stream: logStream }));

That’s it! Now all our production logs go straight to that file, and we can use tools like grep or jq to search through them when we need to. Last month, we had a client complain that their order processing was taking too long, so we grepped the production logs for /api/orders and filtered for responses over 100ms, and found that the payment gateway was the bottleneck. We could’ve spent hours debugging without those logs, but Morgan made it trivial.

Wait, let’s talk about some common mistakes we made when we first started using Morgan, because I don’t want you to repeat them. First, putting Morgan after your routes. We did that once, and all our log entries were empty—Morgan only logs requests that go through middleware that runs before it. So always put Morgan near the top of your middleware stack, right after express.json() and any other parsing middleware, but definitely before your routes. Second, not setting the correct NODE_ENV. If you hardcode Morgan to use dev in production, you’re missing all that critical IP and user agent data. We learned that when a client’s app got a spike in traffic from a known malicious IP, but we didn’t log their IP because we were using dev mode. Oops. Third, overcomplicating the format. We tried adding 10 different tokens at once, and our logs became 200 characters long, which was hard to read. Start simple, then add only the tokens you actually need. If you don’t need the response size, don’t log it—less is more for readability.

Another pro tip: Morgan works great with other logging tools like Winston or Pino, if you need more advanced features, but it’s totally fine to use it on its own. For our smaller client projects, we don’t bother with extra libraries—Morgan gives us everything we need. For enterprise clients, we pair Morgan with Winston to send logs to a monitoring tool like Datadog, but Morgan is still the core because it’s so reliable at capturing request data. It’s also lightweight—adds less than 5KB to your node_modules folder, so it doesn’t slow down your app at all, even when you have hundreds of requests per minute.

Let’s also cover a quick example of how to filter logs, because that’s something we do all the time. Suppose you only want to log error status codes (400 and above) to reduce noise in production. Morgan lets you add an option to skip certain requests. We use this for health checks—our load balancer pings /health every 5 seconds, and those requests fill up the logs with 200 status codes that we don’t need. Here’s how to set that up:
app.use(morgan(‘combined’, {
skip: (req, res) => res.statusCode < 400 && req.url === ‘/health’
}));

That’s perfect! Now we only log /health requests if they return an error, which saves us so much log space. We also use this for static assets—if you serve CSS or JS files, their logs clutter things up, so we skip those too. Just adjust the skip function to whatever makes sense for your app.

If you’re still on the fence about switching to Morgan for your Express apps, let’s be real: custom logging is a pain. Every time you add a new route, you have to remember to add a console.log line, and you’ll forget half the time. Morgan is consistent—every request is logged the same way, every time, so you never miss a thing. As an Express provider, we’ve recommended Morgan to hundreds of clients, and almost all of them have cut their debugging time by at least 30% after switching. One client had a memory leak that they were tracking for weeks, and with Morgan’s detailed logs, they found that a middleware was keeping references to request objects, which was causing the leak. No more guessing—logs told them exactly what was happening.

If you’re running an Express-based business, or just building a personal project that needs solid logging, Morgan is the tool you need. It’s not fancy, it’s not overcomplicated, it just works. We’ve used it on everything from small side projects to enterprise-grade client apps, and it’s never let us down.

If you’re looking to level up your Express apps with reliable logging, or need help integrating Morgan or any other middleware into your stack for your project, our team is here to help. We work with all kinds of Express projects, from startups to large corporations, and we can tailor a logging solution that fits your specific needs. Hit us up to chat through how we can make your app’s logging work for you, not against you.

Express FEDEX References:

  1. Morgan Official Documentation
  2. Express.js Middleware Guide
  3. Node.js fs Module DocumentationReferences:
  4. Morgan Official Documentation
  5. Express.js Middleware Guide
  6. Node.js fs Module Documentation

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/