How to Add Logging to Your Next.js App in 5 Minutes
A step-by-step guide to adding real logging to your Next.js application. Covers App Router, Server Actions, API routes, and middleware — with centralized log search, alerts, and tracing you can set up in minutes, not hours.
If you’re building with Next.js, there’s a good chance your current logging strategy looks something like this: console.log scattered through your Server Components, a few console.error calls in your API routes, and a vague hope that Vercel’s function logs will be enough when something goes wrong in production. It works, sort of, until you’re staring at a wall of unstructured text at 11 PM trying to figure out why a specific user’s checkout failed three hours ago.
The problem isn’t that console.log is bad — it’s that it doesn’t scale beyond your terminal. You can’t search it. You can’t filter by severity. You can’t set up an alert that pings you in Slack when errors spike. You can’t correlate a log message in your API route with the Server Action that triggered it. And in production, those logs are ephemeral — they disappear when your serverless function recycles, which on platforms like Vercel happens constantly.
This guide will walk you through adding real, production-grade logging to a Next.js app. We’ll use Loggy because it takes about five minutes and we obviously think it’s a good fit, but the patterns here — centralized logging, structured metadata, log levels, and request correlation — are valuable regardless of which tool you choose.
The Fastest Path: The CLI
If you want to go from zero to logging in under a minute, the Loggy CLI is the way to go. It detects that you’re running Next.js, installs the SDK, creates a project, and wires everything up automatically.
npx @loggydev/loggy-cli init
The CLI will walk you through authenticating (it opens your browser), detect your Next.js project, install @loggydev/loggy-node, create a project on Loggy, and save your access token to .env.local. When it’s done, you’ll have a working setup and can skip ahead to the “What to Log” section of this post.
But if you prefer to understand every step (which we recommend, honestly), let’s do it manually.
Step 1: Install the SDK
npm install @loggydev/loggy-node
Or with yarn or pnpm — whatever your project uses. The package has minimal dependencies and works in both Node.js server environments and edge runtimes.
Step 2: Create a Loggy Instance
Create a file that initializes your logger. We recommend putting it somewhere central that your whole app can import from.
// src/lib/loggy.ts
import { CreateLoggy } from "@loggydev/loggy-node";
export const loggy = CreateLoggy({
identifier: "my-nextjs-app",
accessToken: process.env.LOGGY_ACCESS_TOKEN,
level: process.env.NODE_ENV === "production" ? "info" : "debug",
});
A few things worth explaining here. The identifier is a human-readable name that shows up in your Loggy dashboard — use your app name or service name. The accessToken is your project token from Loggy, which you’ll get when you create a project (either through the CLI or the dashboard). And the level setting controls which logs actually get sent — in development you probably want debug-level verbosity, but in production you can filter that down to info and above to reduce noise.
The access token should live in your .env.local file:
# .env.local
LOGGY_ACCESS_TOKEN=your-project-token-here
Make sure .env.local is in your .gitignore (it is by default in Next.js). Never commit tokens to your repository.
Step 3: Log From Anywhere in Your App
Now you can import and use your logger anywhere in your Next.js application. Let’s look at the most common patterns.
In Server Components
// src/app/dashboard/page.tsx
import { loggy } from "@/lib/loggy";
export default async function DashboardPage() {
loggy.info("Dashboard page loaded");
const data = await fetchDashboardData();
loggy.debug("Dashboard data fetched", { itemCount: data.items.length });
return <Dashboard data={data} />;
}
Server Components run on the server, so your logs go directly to Loggy without any client-side exposure. This is one of the nice things about Next.js’s architecture — your logging code never ships to the browser.
In API Routes
// src/app/api/webhooks/stripe/route.ts
import { loggy } from "@/lib/loggy";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body = await request.json();
loggy.info("Stripe webhook received", {
type: body.type,
id: body.id,
});
try {
await processWebhook(body);
loggy.info("Webhook processed successfully", { type: body.type });
return NextResponse.json({ received: true });
} catch (error) {
loggy.error("Webhook processing failed", {
type: body.type,
error: error instanceof Error ? error.message : "Unknown error",
});
return NextResponse.json(
{ error: "Processing failed" },
{ status: 500 }
);
}
}
Notice the structured metadata — { type: body.type, id: body.id }. This is one of the most valuable patterns in logging. Instead of embedding data in the message string, pass it as a metadata object. This makes your logs searchable and filterable in the Loggy dashboard. You can search for all webhooks of type invoice.paid or filter to only see failed webhook processing.
In Server Actions
// src/app/actions/create-order.ts
"use server";
import { loggy } from "@/lib/loggy";
export async function createOrder(formData: FormData) {
const productId = formData.get("productId") as string;
const quantity = Number(formData.get("quantity"));
loggy.info("Order creation started", { productId, quantity });
try {
const order = await db.orders.create({
data: { productId, quantity },
});
loggy.info("Order created successfully", {
orderId: order.id,
productId,
quantity,
});
return { success: true, orderId: order.id };
} catch (error) {
loggy.error("Order creation failed", {
productId,
quantity,
error: error instanceof Error ? error.message : "Unknown error",
});
return { success: false, error: "Failed to create order" };
}
}
Server Actions are one of the places where logging matters most, because they’re handling user-initiated mutations. When a customer says “I tried to place an order and nothing happened,” you want to be able to search your logs for their order attempt and see exactly what went wrong.
In Middleware
// src/middleware.ts
import { CreateLoggy } from "@loggydev/loggy-node";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const loggy = CreateLoggy({
identifier: "my-nextjs-app",
accessToken: process.env.LOGGY_ACCESS_TOKEN,
level: "info",
});
export function middleware(request: NextRequest) {
const start = Date.now();
loggy.info("Request received", {
method: request.method,
path: request.nextUrl.pathname,
userAgent: request.headers.get("user-agent")?.substring(0, 100),
});
const response = NextResponse.next();
loggy.debug("Request completed", {
method: request.method,
path: request.nextUrl.pathname,
duration: Date.now() - start,
});
return response;
}
export const config = {
matcher: ["/api/:path*", "/dashboard/:path*"],
};
Note that we create a separate Loggy instance in middleware rather than importing from src/lib/loggy.ts. This is because Next.js middleware runs in the Edge Runtime, which has a slightly different module resolution. The matcher config ensures we only log requests to routes we care about, not every static asset request.
Simple, beautiful observability for developers
What to Log (And What Not To)
Now that the plumbing is set up, let’s talk about what’s actually worth logging. This is where most teams either over-log (every function entry and exit) or under-log (only errors), and both extremes make debugging harder.
Log business events. User signed up, order placed, payment processed, subscription changed, email sent. These are the events you’ll search for when trying to understand what happened to a specific user. Always include relevant IDs as metadata — user IDs, order IDs, session IDs — so you can trace a user’s journey.
Log integration boundaries. Every time your app talks to an external service — a payment provider, an email API, a third-party data source — log the request and the response (or at least the status). These are the most common failure points, and when Stripe returns a 402 instead of a 200, you want that in your logs with the relevant context.
Log errors with context. Don’t just log error.message. Include what the user was trying to do, what IDs were involved, and any state that would help you reproduce the issue. The difference between “Payment failed” and “Payment failed for user usr_123 on plan Pro, Stripe error: card_declined, card ending 4242” is the difference between hours of debugging and a five-second diagnosis.
Don’t log sensitive data. Never log full credit card numbers, passwords, API keys, or personal data that falls under GDPR or similar regulations. If you need to reference a credit card, log the last four digits. If you need to reference a user, log their user ID, not their email address. This isn’t just good practice — it’s often a legal requirement.
Don’t log in hot loops. If you have a loop that processes 10,000 items, don’t log inside the loop. Log before and after with aggregate information: “Processing 10,000 records” and “Processed 10,000 records in 3.2s, 12 failures.”
Adding Alerts
Once your logs are flowing, the next step is making sure you know when things go wrong without having to watch the dashboard. Loggy’s alerting system lets you set up notifications for error spikes and warning spikes. You configure a threshold (like “more than 10 errors in 5 minutes”), connect a Slack or Discord channel, and you’ll get a notification when something is going sideways.
For a Next.js app, we’d recommend starting with a single alert rule: notify when error-level logs spike above your normal baseline. You can get more specific later, but this one rule catches the vast majority of production incidents.
Adding Tracing
If your Next.js app makes calls to other services — a separate API, a background worker, a microservice — you can add distributed tracing to follow requests across service boundaries. The Loggy SDK supports W3C Trace Context, which means trace IDs propagate automatically through HTTP headers.
// src/lib/loggy.ts - add tracing
import { CreateLoggy, CreateTracer } from "@loggydev/loggy-node";
export const loggy = CreateLoggy({
identifier: "my-nextjs-app",
accessToken: process.env.LOGGY_ACCESS_TOKEN,
});
export const tracer = CreateTracer({
serviceName: "my-nextjs-app",
accessToken: process.env.LOGGY_ACCESS_TOKEN,
});
This is entirely optional and something you can add later when you need it. Start with logging. Get comfortable searching and filtering your logs. Add tracing when you find yourself wondering “what happened between when my API received this request and when my background worker processed it.”
The Result
After about five minutes of work, here’s what you have: every important event in your Next.js app is logged with structured metadata, searchable in real-time from the Loggy dashboard. You can filter by log level, search by user ID, see errors spike in real-time, and get pinged in Slack when something breaks. And you did it without configuring a single YAML file, installing a server agent, or signing up for a platform that charges you by the gigabyte.
That’s the whole point. Logging shouldn’t be a project. It should be a five-minute setup that quietly watches your app and speaks up when it matters.
If you want to try it, create a free Loggy account — no credit card required — and run npx @loggydev/loggy-cli init in your Next.js project. Your first logs will appear in the dashboard within seconds.