Logging route handler responses in Next.js 14

June 19, 2024

Switching from Express.js to Next.js for your server-side needs can be quite the transition, especially when it comes to maintaining the same level of request logging. In Express.js, tools like morgan or express-request-logger are commonly used to log requests, but Next.js requires a different approach. Here’s how you can achieve similar logging functionality in your Next.js application:

Creating a Logging Wrapper

Next.js does not support traditional middleware for intercepting responses. However, you can still log requests by wrapping your route handlers in a higher-order function. Let's create a function called withLogging to wrap around our route handlers. This function will log the request details and response body while ensuring that the response stream remains accessible and unchanged.

import { NextRequest, NextResponse } from 'next/server';
 
const withLogging = (handler: (req: NextRequest) => Promise<NextResponse>) => {
	return async (req: NextRequest) => {
		// Call the handler and get the response
		const res = await handler(req);
 
		// Clone the response to avoid locking the stream
		const cloneResponse = res.clone();
 
		// Extract and log the response body
		const outputBody = await cloneResponse.text();
 
		// Create log items
		const logItems = [
			req.method,
			req.nextUrl.toString(),
			res.status,
			outputBody,
		];
 
		// Log the request and response details
		console.log(logItems.join(" - "));
 
		return res;
	};
};

Next, we wrap our route handlers with the withLogging function. Here's an example of how to wrap a POST handler:

import { NextRequest, NextResponse } from 'next/server';
 
export const POST = withLogging(async (req: NextRequest) => {
  return NextResponse.json({ message: "Hello, World!" });
});

When a POST request is made to this endpoint, the console will log something like this:

POST - http://localhost:3000/api - 200 - {"message":"Hello, World!"}

Why Clone the Response?

In Next.js, the body of the response is a ReadableStream. Once you read from the stream, it becomes locked, which can lead to errors if Next.js try to use it again. Cloning the response allows you to read the body without locking the original stream, ensuring the response can be returned untouched.

Here’s the relevant error you might encounter without cloning:

Error: failed to pipe response
 
[cause]: TypeError [ERR_INVALID_STATE]: Invalid state: The ReadableStream is locked
 

Cloning the response and then accessing its body ensures that the response remains in a valid state, especially for more complex responses like redirects or rewrites.

Performance Overhead

There is a slight performance overhead due to cloning the response and logging the data. However, this trade-off is generally acceptable given the benefits of improved visibility and easier debugging.

Environment-Based Logging

To control logging based on the environment, you can check process.env and conditionally apply the logging wrapper. This allows you to enable detailed logging in development while keeping production logs clean and efficient.

const withLogging = (handler: (req: NextRequest) => Promise<NextResponse>) => {
  return async (req: NextRequest) => {
    if (process.env.NODE_ENV === 'development') {
      const res = await handler(req);
      const cloneResponse = res.clone();
 
      const outputBody = await cloneResponse.text();
 
      const logItems = [
        req.method,
        req.nextUrl.toString(),
        res.status,
        outputBody,
      ];
 
      console.log(logItems.join(" - "));
 
      return res;
    } else {
      return handler(req);
    }
  };
};
 

This setup ensures that logging can be adjusted according to the environment, minimizing unnecessary overhead in production.

Customizing Logs

You can customize the logged items by adding more details to the logItems array. Be cautious to avoid logging sensitive information that could pose security risks or clutter the logs.

This approach provides a robust logging solution for Next.js applications, enabling better request tracking and response handling while maintaining application performance and security.