Next.js Code Audit: What We Check in a Production App, and How to Start Yourself

What a Next.js code audit checks: server/client boundaries, Server Actions, caching from Next.js 14 to 16, bundle size, upgrades and self-hosting.

Key Takeaways

  • A Next.js audit is not a generic code review. Most serious findings sit in places specific to the framework: the server/client boundary, Server Actions, caching, and how the app is deployed.
  • Caching rules changed twice in two years. Next.js 15 stopped caching fetch, GET Route Handlers and page navigations by default; Next.js 16 made caching opt-in with "use cache". Apps upgraded without revisiting caching often serve stale or wrongly shared data.
  • Excessive "use client" is the most common performance finding: it ships server logic and large libraries to the browser and slows every page.
  • Treat every Server Action and Route Handler as a public endpoint. The framework's own docs say so.
  • You can run a first pass yourself in an afternoon with next build, a bundle analyser, a dependency audit and a few searches. The checks are below.

A Next.js code audit is a structured review of a Next.js application's code, configuration and deployment, aimed at finding what will hurt in production: security gaps, performance problems, data leaks between users, fragile upgrades and operational blind spots. It is narrower and deeper than a general technical audit of a web application, because the framework blurs the line between server and browser in ways that create their own classes of bugs. This article lists what we check, why each area matters, and what you can look at yourself before hiring anyone. For security hardening specifically, our Next.js security checklist goes further.

1. Version, router and upgrade path

We start with what the app is built on, because everything else depends on it.

  • Next.js and React versions. How far behind is the app, and are any published vulnerabilities unpatched? npx next info prints the versions in use.
  • App Router, Pages Router, or both. Mixed apps are common after partial migrations; we check whether the split is deliberate and documented.
  • Breaking changes not yet absorbed. Next.js 15 made request APIs (cookies(), headers(), params, searchParams) asynchronous, with synchronous access temporarily allowed behind warnings; Next.js 16 removed that synchronous access, renamed middleware.ts to proxy.ts, made Turbopack the default bundler and requires Node.js 20.9 or later (Next.js 15, Next.js 16). An app with warnings in its logs is an app whose next upgrade will break.

Deliverable: a realistic upgrade path, with the effort for each step.

2. The server/client boundary

This is where we find the most issues, in both performance and security.

  • "use client" sprawl. Marking a component as a client component makes everything it imports part of the browser bundle. We regularly find "use client" at the top of layouts or large page components, pulling date libraries, markdown parsers or entire UI kits into every page. The fix is usually to push the directive down to the small interactive pieces.
  • Server-only code reaching the browser. Database clients, internal API helpers or secret-reading modules imported by something that ends up client-side. The server-only package turns that mistake into a build error; we check whether it's used.
  • Over-sharing props. Server components that pass a full database record to a client component send every field to the browser, visible in the page payload, even if the UI shows two of them.
  • Environment variables. Anything prefixed NEXT_PUBLIC_ is inlined into client JavaScript. We list every one and check none is a secret.

3. Server Actions and Route Handlers

Next.js documents that a Server Action is reachable as a public HTTP endpoint even if nothing in your UI calls it, and recommends treating it as one (Next.js 15 release notes). For each action and route handler we check:

  1. Authentication inside the action itself, not only in the page that renders the form, and not only in proxy.ts.
  2. Authorization: can this user act on this record? Missing ownership checks are the classic finding: change an ID in the request and edit someone else's data.
  3. Input validation with a schema, not trust in the form.
  4. Rate limiting on anything that sends email, creates accounts or costs money.
  5. What the action returns, since return values go to the browser.

We cover the pattern for structuring these checks in structuring authorization.

4. Data fetching and caching

Caching is where Next.js apps most often behave differently from what their authors think, partly because the defaults changed:

VersionDefault behaviour
Next.js 13–14 (App Router)fetch results and GET Route Handlers cached by default unless opted out
Next.js 15GET Route Handlers and page segments in the client router cache no longer cached by default; fetch caching becomes opt-in
Next.js 16Cache Components: caching is explicit with "use cache", and dynamic code runs at request time by default; revalidateTag() takes a cache profile, updateTag() gives read-your-writes in Server Actions

What we look for:

  • User-specific data in a shared cache. The serious one: a cached function or page that reads the current user, so one user's data is served to another.
  • Stale data after writes, because nothing revalidates the right tag or path.
  • Request waterfalls: sequential awaits in a page or layout that could run in parallel, or data fetched in a layout and again in each page.
  • Everything dynamic by accident. One call to cookies() in a root layout can make every route dynamic. The route table printed by next build shows which routes are static and which aren't.

5. Performance and bundle size

  • JavaScript per route from the build output and a bundle analyser, compared with what each page actually needs.
  • Images: next/image used with correct sizes, a priority hint on the largest above-the-fold image, and remotePatterns restricted (the older images.domains is deprecated in Next.js 16).
  • Fonts loaded through next/font rather than external stylesheets.
  • Third-party scripts (analytics, chat widgets, tag managers) loaded with an appropriate next/script strategy.
  • Core Web Vitals from real users where available (Search Console, CrUX), lab tests otherwise, with Interaction to Next Paint on the heaviest interactive pages.

6. Deployment and operations

Many Next.js issues only appear once the app leaves Vercel or runs on more than one instance.

  • Self-hosting setup: output: "standalone", Node.js version, how static assets are served, and whether a CDN sits in front.
  • Shared cache across instances. With several containers, incremental static regeneration and the data cache need a shared cache handler; otherwise each instance serves its own version of a page.
  • Observability: is instrumentation.ts used, and does the onRequestError hook report server errors somewhere someone reads?
  • Security headers and CSP set once, centrally.
  • Build reproducibility: lock file committed, CI building the same way production does, preview environments.

7. Code health

The usual checks still apply: TypeScript strictness, test coverage on business rules, error boundaries (error.tsx, not-found.tsx), accessibility basics, and whether a new engineer could get the app running from the README.

A first pass you can run yourself

You don't need an auditor to start. In an afternoon:

  1. Versions: npx next info, then compare with the latest release and the security advisories.
  2. Route table: run next build and read the output. Are the routes you expect to be static actually static? Are any first-load bundles unexpectedly large?
  3. Bundle: add @next/bundle-analyzer and look for big libraries in client bundles.
  4. Dependencies: npm audit --omit=dev (or your package manager's equivalent).
  5. Searches across the codebase:
    • "use client" in layouts or very large files;
    • NEXT_PUBLIC_ and check each value is safe to publish;
    • "use server" and open each action to confirm it checks the user and their rights;
    • dangerouslySetInnerHTML and confirm the HTML is sanitised;
    • cookies() or headers() in root layouts.
  6. Logs: look for deprecation warnings in development and production output; each one is a future breaking change.

If those checks raise more questions than they answer, that's the point at which an outside review pays for itself.

What a full audit delivers

A written report with each finding rated by severity and effort, the evidence (file and line), and a recommended fix; a short list of what to fix this week; an upgrade plan; and a walkthrough with your team. On a typical Next.js application this takes one to three weeks, depending on size. The technical audit page describes how we run it.

Frequently asked questions

How is a Next.js audit different from a general code review? A general review looks at code quality. A Next.js audit also looks at how code is split between server and browser, how caching behaves across versions, how Server Actions are secured and how the app runs when deployed. Those are where framework-specific production problems come from.

Should we upgrade to Next.js 16 before or after an audit? After, usually. The audit tells you what the upgrade will touch and what to fix first, so you don't carry existing problems into the new version.

We're on the Pages Router. Is that a problem? Not in itself. The Pages Router is still supported. The question is whether a migration would solve a real problem for you; an audit should answer that, not assume it.

Can you audit an app hosted on Vercel? Yes. Hosting changes some of the deployment checks, but the code, caching, security and performance reviews are the same.


We build and run Next.js applications in production, including this site. If you want an independent review of yours, see our technical audit service or get in touch.

Sources