Beginner

Structured Logging in Express.js

Add request logging middleware, capture uncaught exceptions, and send structured logs with trace IDs to LogFlow from your Express.js application.

LogFlow TeamMay 20, 202610 minExpressNode.js

Structured logging in Express.js means every request, error, and event is captured as a JSON object — not a plain text line — so you can filter, search, and correlate logs in LogFlow without parsing strings.

Prerequisites

  • Node.js 18+ project with Express.js
  • A LogFlow account (free tier works)
  • Your API key from Settings → API Key

Step 1 — Install the SDK

npm install @getlogflow/js

Step 2 — Create a logger module

Create src/lib/logger.js (or .ts for TypeScript):

import { LogFlow } from '@getlogflow/js'

export const logger = new LogFlow({
  apiKey: process.env.LOGFLOW_API_KEY,
  service: 'api',           // rename to your service name
  flushInterval: 3000,
  maxRetries: 3,
})

// Flush on shutdown so no logs are lost
process.on('SIGTERM', async () => {
  await logger.destroy()
  process.exit(0)
})

Add your API key to .env:

LOGFLOW_API_KEY=lf_your_api_key_here

Step 3 — Add request logging middleware

Add this middleware before your routes. It logs every request with method, path, status code, and response time:

import express from 'express'
import { logger } from './lib/logger.js'

const app = express()

// Request logging middleware
app.use((req, res, next) => {
  const start = Date.now()
  const { method, path, ip } = req

  res.on('finish', () => {
    const duration = Date.now() - start
    const level = res.statusCode >= 500 ? 'error'
                : res.statusCode >= 400 ? 'warn'
                : 'info'

    logger[level](`${method} ${path} ${res.statusCode}`, {
      method,
      path,
      statusCode: res.statusCode,
      durationMs: duration,
      ip,
    })
  })

  next()
})

Step 4 — Add a trace ID to each request

Trace IDs let you group all logs from a single request together in LogFlow. Install a UUID library or use Node's built-in crypto:

import { randomUUID } from 'crypto'

app.use((req, res, next) => {
  req.traceId = req.headers['x-trace-id'] || randomUUID()
  res.setHeader('x-trace-id', req.traceId)
  next()
})

Option A — pass traceId per call:

logger.info('User signed up', {
  traceId: req.traceId,
  userId: user.id,
  email: user.email,
})

Option B — use a child logger (recommended):

Create a per-request child logger that carries traceId automatically — no need to repeat it on every call:

app.use((req, res, next) => {
  req.traceId = req.headers['x-trace-id'] || randomUUID()
  res.setHeader('x-trace-id', req.traceId)
  // Child logger merges traceId into every log it produces
  req.log = logger.child({ traceId: req.traceId })
  next()
})

app.post('/orders', async (req, res) => {
  req.log.info('Order received')                           // traceId included automatically
  const order = await createOrder(req.body)
  req.log.info('Order created', { orderId: order.id })    // traceId + orderId
  res.json(order)
})

In LogFlow's Logs Explorer, click any log and hit View trace to see all logs from that request.

Step 5 — Capture unhandled errors

Add a global error handler after all routes. This catches any error passed to next(err) and logs it before returning a 500:

// Error handler — must have 4 parameters
app.use((err, req, res, next) => {
  logger.error(err.message || 'Unhandled error', {
    traceId: req.traceId,
    stack: err.stack,
    path: req.path,
    method: req.method,
  })
  res.status(500).json({ error: 'Internal Server Error' })
})

// Catch uncaught exceptions
process.on('uncaughtException', (err) => {
  logger.fatal('Uncaught exception', { stack: err.stack, message: err.message })
  process.exit(1)
})

process.on('unhandledRejection', (reason) => {
  logger.error('Unhandled promise rejection', { reason: String(reason) })
})

Step 6 — Log business events

Beyond HTTP requests, log important business events with structured metadata:

app.post('/orders', async (req, res) => {
  const order = await createOrder(req.body)

  logger.info('Order created', {
    traceId: req.traceId,
    orderId: order.id,
    userId: req.user.id,
    amount: order.totalCents,
    currency: order.currency,
  })

  res.json(order)
})

Verify it's working

Start your server and make a request. Open LogFlow → Logs Explorer — you should see your request logs appear within a few seconds.

Filter by service: type service:api in the search bar to see only your Express logs.

FAQ

Should I log request bodies? Be careful — request bodies often contain passwords and personal data. Log only the fields you need (like user ID, order ID) rather than the entire body.

What log level should I use? Use info for normal operations, warn for recoverable issues (rate limits, retries), and error for failures that affect users. Reserve fatal for errors that crash the process.

How do I test that logs are reaching LogFlow? Check the Live Tail page in your dashboard — it shows logs in real time as they arrive.

Start monitoring your logs today

Free plan available. No credit card required. Up and running in 2 minutes.

Get started free