Skip to content

Tutorial: Add Logging to Your Server

In this tutorial you will add request logging to a Remix server, customize the output format, skip logging for certain requests, and write logs to a file for production use.

Prerequisites

Step 1: Add the Logger Middleware

ts
import { createRouter } from 'remix/fetch-router'
import { logger } from 'remix/logger-middleware'
import { route } from 'remix/fetch-router/routes'

let routes = route({
  home: '/',
  posts: '/posts',
})

let router = createRouter({
  middleware: [
    logger(),
  ],
})

router.get(routes.home, () => new Response('Home'))
router.get(routes.posts, () => new Response('Posts'))

Start your server and make some requests. You will see colorized output in your terminal:

GET / 200 3ms
GET /posts 200 5ms
GET /favicon.ico 404 1ms

Step 2: Choose a Format for Production

The default 'dev' format uses ANSI colors that look great in a terminal but clutter log files. Switch to 'short' or 'combined' for production:

ts
logger({
  format: process.env.NODE_ENV === 'production' ? 'combined' : 'dev',
})

The 'combined' format includes client IP, timestamp, and user agent -- useful for traffic analysis and debugging:

127.0.0.1 - [08/Apr/2026:12:00:00 +0000] "GET /posts HTTP/1.1" 200 1234 "-" "Mozilla/5.0..."

Step 3: Skip Logging for Static Assets

In development, static file requests can flood your logs. Use the skip option to filter them out:

ts
logger({
  skip(context) {
    let path = context.url.pathname
    return path.startsWith('/assets/') || path === '/favicon.ico'
  },
})

The skip function receives the request context and returns true to suppress logging for that request.

Step 4: Create a Custom JSON Format

For structured logging (useful with log aggregation services like Datadog or CloudWatch), output JSON:

ts
logger({
  format(info) {
    return JSON.stringify({
      timestamp: new Date().toISOString(),
      method: info.method,
      url: info.url,
      status: info.status,
      responseTime: info.responseTime,
      contentLength: info.contentLength,
      userAgent: info.userAgent,
    })
  },
})

Each log line is a single JSON object, making it easy to parse and search.

Step 5: Write Logs to a File

Use the output option to write logs to a file instead of (or in addition to) the console:

ts
import { appendFileSync } from 'node:fs'

logger({
  format: 'combined',
  output(message) {
    appendFileSync('/var/log/myapp/access.log', message + '\n')
  },
})

For both console and file output:

ts
logger({
  format: 'short',
  output(message) {
    console.log(message)
    appendFileSync('/var/log/myapp/access.log', message + '\n')
  },
})

Step 6: Middleware Order

Place logger() first in the middleware chain so it measures the total response time including all downstream processing:

ts
let router = createRouter({
  middleware: [
    logger(),          // First: captures total time
    compression(),     // Compression time is included
    staticFiles('./public'),
    csrf(),
    formData(),
  ],
})

If you place logger() after other middleware, the logged response time will not include the time spent in those earlier middleware.

What You Learned

  • How to add request logging with different formats.
  • How to skip logging for specific request patterns.
  • How to create a custom JSON log format for structured logging.
  • How to write logs to a file.
  • Why middleware order matters for accurate response time measurement.

Next Steps

Released under the MIT License.