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
- A Remix project with a working server (see the Getting Started guide).
Step 1: Add the Logger Middleware
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 1msStep 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:
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:
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:
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:
import { appendFileSync } from 'node:fs'
logger({
format: 'combined',
output(message) {
appendFileSync('/var/log/myapp/access.log', message + '\n')
},
})For both console and file output:
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:
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
- API Reference -- Full details on formats, options, and the
LogInfoobject. - async-context-middleware -- Access request context from deep utility functions for richer logging.