Skip to content
Essay9 min readCritique

How AI Code Generators Create Circular Imports in Next.js (and How to Spot Them)

AI is fast at files and slow at architecture. The result is circular dependency spaghetti that `next dev` hides until production build explodes.

AI code generators are excellent at producing plausible files quickly and terrible at preserving module boundaries. In Next.js apps, that gap shows up as circular imports: `auth.ts` imports `db.ts`, `db.ts` imports `session.ts`, and `session.ts` imports `auth.ts` again. The app still runs in `next dev`. Then Vercel build fails with a TypeScript or dynamic import error nobody can reproduce locally.

Barrels
Re-export hubs that turn two-file imports into graph-wide loops.
Config hubs
Central `config.ts` files that every module imports and that import back.
Provider triangles
React context providers that import hooks that import the provider.
Duplicate utils
Same helper copied into two modules that later cross-import.
Server/client crossings
"use client" and server modules importing each other through shared barrels.

Pattern one is the barrel file. AI loves `export * from "./foo"` because it makes imports look clean. One new export and suddenly `lib/index.ts` pulls in a server module that imports a client component that imports the barrel again. Pattern two is the config hub: the model creates `lib/env.ts` or `lib/config.ts` and wires every feature through it, then adds feature-specific imports back into config for "convenience."

Pattern three is the provider triangle. A new `AuthProvider` imports `useSession` from `hooks/use-session.ts`, but the hook imports the provider context from the same file tree root. Pattern four is duplicate utilities: two files get nearly identical `formatDate` helpers, then a refactor merges usage and creates a bidirectional import. Pattern five is the server/client boundary violation — especially common when AI adds `"use client"` to a file that used to be server-only without untangling the import graph.

Development mode evaluates modules lazily and tolerates initialization order quirks that production bundling does not. A circular graph can appear to work until a specific export is first accessed during SSR, tree-shaking removes a side effect the cycle depended on, or TypeScript emits a value import where `import type` was required. That is why teams search for "next js dynamic import typescript error" after green local runs and red Vercel deploys.

madge vs dependency-cruiser

Both belong in CI. They optimize for different questions.

ToolBest forTradeoff
madgeFast circular dependency reports and SVG graphs for quick PR triage.Less opinionated about forbidden import rules (server/client, layer boundaries).
dependency-cruiserPolicy-as-code: ban cycles, enforce folder layers, flag orphan modules.Heavier setup; worth it once the repo has import architecture rules.
next buildGround truth for what production will compile and bundle.Slower; run on changed apps or in sandbox review, not every save.

Run graph analysis on the PR diff path, then confirm with `next build` before merge. Dev server green is not evidence.

Break cycles without making new ones
  1. 1
    Can this import be type-only?
    Replace value imports with `import type` when only types cross the boundary. This breaks many TypeScript emit cycles without runtime changes.
  2. 2
    Are types mixed with runtime code?
    Extract shared interfaces to `types.ts` or `*.types.ts` files that import nothing from feature modules.
  3. 3
    Is a barrel file involved?
    Import from the concrete module path instead of `index.ts`. Remove or narrow barrel re-exports on hot paths.
  4. 4
    Is initialization order the real issue?
    Move shared constants or factory functions to a leaf module both sides can import. Avoid top-level side effects in imported files.
  5. 5
    Did the fix survive production build?
    Re-run madge or dependency-cruiser and `next build`. A cycle "fixed" only in dev is not fixed.

Common cycle shapes in Next.js

Left: barrel loop. Right: type-only fix.

// lib/index.ts
export * from './auth'
export * from './db'

// lib/auth.ts
import { getUser } from '@/lib' // pulls db → session → auth

// lib/db.ts
import { getSession } from '@/lib/auth'

Circular imports accumulate across AI-assisted refactors. The reliable gate is PR-time: graph check on the diff, required `next build`, and review comments that flag new barrels or config-hub imports. Static AI review cannot prove the bundler agrees — that requires running the build.

Critique runs sandbox verification on pull requests so TypeScript compile errors and Next.js build failures surface before merge, not after Vercel emails the team. Pair graph tooling in CI with runtime proof when AI generators are writing most of the imports.

Dev mode and production bundling use different module evaluation and tree-shaking paths. SSR and static analysis in `next build` expose cycles that lazy dev loading masked.
Find the smallest cycle with madge, switch cross-boundary imports to `import type`, extract shared types to a leaf file, and stop importing through barrel `index.ts` files.
Add madge or dependency-cruiser to CI, fail on new cycles, and run `next build` on the changed app. For AI-heavy repos, add sandbox review that compiles the PR branch.
It can flag suspicious import patterns and new barrels, but only a dependency graph tool plus a production build proves the cycle is gone.

Catch circular imports before Vercel does

Install Critique on GitHub and run sandbox build verification on the next AI-generated PR. Graph tools find cycles; runtime proof confirms the fix.

Start free