1. Omid Sayfun
  2. /
  3. Notebook
  • Home
  • About
  • Notebook
  • Token Usage
  • Whisper Usage
Tools
  • Agent Knowledge Base

Logging route handler responses in Next.js 14

June 19, 2024 · Updated on August 09, 2026

Next.js middleware cannot log response bodies. It runs before the route handler, so by the time a response exists the middleware has already returned. That rules out the Express.js pattern, where morgan or express-request-logger sit in the middleware chain and see both sides of the request. In Next.js 14 you get the same request logging by wrapping the route handler itself.

I write short, practical notes like this one. Get the next one by email:

Unsubscribe anytime.

Wrap the route handler in a higher-order function

A higher-order function takes the route handler, runs it, and returns the same response after logging. Call it withLogging. It logs the request details and the response body, and it leaves the response stream 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, wrap the route handler with withLogging. The POST handler below returns NextResponse.json, the standard route handler response in Next.js 14:

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!"}

Clone the response or the stream locks

In Next.js, the body of the response is a ReadableStream. Once you read from the stream it becomes locked, and Next.js throws when it tries to use it again. Cloning the response lets you read the body without locking the original stream, so the handler's response is 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.

Cloning costs a small amount of performance

Cloning the response and reading the body adds work to every request. The cost is small, and the extra visibility during debugging is usually worth it.

Log only in development

To control logging by environment, check process.env and apply the wrapper conditionally. Detailed logs stay in development, and production logs stay clean.

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.

Add fields to logItems, but not secrets

You can add more details to the logItems array: headers, timing, a request id. Leave out tokens, passwords, and anything else that should not sit in a log file.

That is the whole setup. If you want structured output instead of a line of text, swap console.log for a Next.js logger such as pino or winston; the wrapper around the route handler does not change.

Join My Newsletter

Occasional notes on software, tools, and things I learn. No spam.

Unsubscribe anytime.

Continue Reading

  • Embeddings rot too. Running pgvector in production.Aug 28, 2026
  • Stop shipping retrieval changes on vibesAug 25, 2026
  • Your RAG is confidently wrong without hybrid searchAug 21, 2026
  • Stop tuning everything. pgvector has three knobs that matter.Aug 18, 2026
  • Your agent's knowledge base is lying to you. Run these 14 checks.Aug 18, 2026