Skip to content

feat(graph): add Next.js App Router route resolver - #179

Open
abhinav-phi wants to merge 2 commits into
mex-memory:mainfrom
abhinav-phi:feat/nextjs-app-router-resolver
Open

feat(graph): add Next.js App Router route resolver#179
abhinav-phi wants to merge 2 commits into
mex-memory:mainfrom
abhinav-phi:feat/nextjs-app-router-resolver

Conversation

@abhinav-phi

Copy link
Copy Markdown
Contributor

Resolves #95.

What

A bounded Next.js resolver following the express.ts/flask.ts pattern. It turns App Router route modules into route nodes and connects exported HTTP handler functions:

  • Recognizes app/**/route.ts and route.js, including src/app roots
  • One stable route node per exported handler for GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD — declared as export async function GET() or export const GET = async () => ...
  • Route path derived from the route file's directory: app/api/users/route.ts/api/users
  • Dynamic segment text ([id]) and catch-alls ([...slug]) preserved verbatim; route groups ((marketing)) excluded from paths the way Next itself resolves them
  • Only explicitly exported HTTP-handler names create route nodes; a same-file helper() export is ignored
  • Detection keys on the next dependency in package.json or the presence of route files in the tree (positive and negative cases tested)
  • Same-file handlers resolve only when unambiguous; missing or duplicate handlers stay unresolved

Out of scope, per the issue: Pages Router, route groups' URL composition nuances, parallel/intercepting routes, rewrites, middleware, server components, and any change to the TypeScript extractor or graph core. No identity, reconciliation, schema, or drift-semantics changes.

Tests

  • resolver-nextjs.test.ts (11): detection positive/negative (manifest and file-based), path derivation for both roots plus route groups/dynamic/catch-all segments and rejection of non-App-Router paths, function and arrow-function exports, JS route files, non-route files ignored, resolution and ambiguity, registry registration.
  • resolver-nextjs-integration.test.ts (1): real rebuildGraph over a Next.js fixture — route nodes persist with the stable nextjs-route identity and both handlers resolve through framework edges.

npm run typecheck, npm run build pass; resolver suites green locally (the pre-existing Windows symlink/WAL failures in test/graph-integration.test.ts reproduce identically on clean main and are unrelated).

@theyashasvipandey

Copy link
Copy Markdown
Collaborator

Thanks @abhinav-phi , this is a clean, well-scoped resolver. It follows the Express pattern, stays additive, and the tests line up with #95. I ran the resolver suites (all green, Express unaffected) and then real rebuildGraph builds over a few small Next.js projects. Two things need fixing before merge, plus a few smaller ones.

Must fix

1. A repeated handler declaration fails the whole graph build

extract emits one route node per matching line, and every node for the same method in a file gets the same id. The engine rejects duplicate ids, so the entire build throws. TypeScript overloads are enough:

// app/api/x/route.ts
export function GET(a: Request): Response;
export function GET(a: Request, b: unknown): Response;
export function GET(a: Request, b?: unknown): Response { return new Response(); }
Graph staging invariant failed: duplicate node id route:d9f8ee048e9b2ad38ea0ea9aed8230d5.

A handler left inside a block comment does the same. Since a route module can only export one GET, emitting at most one route node per method per file fixes it. Please add a test with the overload case.

2. Route paths are wrong in monorepos

deriveRoutePath checks for an app segment, but then cuts at indexOf(appRoot) — the first substring match:

file derived expected
apps/web/app/api/orders/route.ts /s/web/app/api/orders /api/orders
packages/webapp/app/api/users/route.ts /app/api/users /api/users

The first reproduces end to end, and apps/web/app is the standard monorepo layout. Finding the index of the app segment (checking src/app first) fixes both; please add these paths to the test.

Should fix

3. Typed const handlers are missed

export const GET: RouteHandler = async () => ... creates no route, because EXPORTED_ARROW expects = right after the name — even though the TypeScript extractor already creates the GET function node. Allowing an optional : Type covers it.

Other common forms — export const { GET, POST } = handlers, export const GET = withAuth(...), export { handler as GET } — produce no route either. Emitting the route and leaving the handler unresolved would match #95's rules better; fine as a follow-up.

Smaller things

  • Edge label. resolvedBy: "framework" doesn't say which resolver made the edge; nextjs-route-handler would match Express (which also uses confidence 0.8 for the same evidence, vs 1 here). The integration test would need updating.
  • Private folders. Next.js excludes _folder segments from routing, so app/_lib/route.ts shouldn't become /_lib.
  • Detection. The issue asks for detection via the next dependency; the PR also enables on any route.* file. Makes sense for monorepos, but please note the reasoning in a comment.
  • JS fixture. nextjs-items-route.js contains TypeScript annotations, so it isn't valid JavaScript.
  • CHANGELOG conflict. Your entry's section was released as 0.8.1; rebasing and moving it under ## [Unreleased] (new ### Added heading) resolves the only conflict.

Translate file-based route modules into route nodes and connect
exported HTTP handler functions without modeling the full Next.js
routing system.

The resolver recognizes app/**/route.ts|js including src/app roots,
emits one route node per exported HTTP handler (GET through HEAD,
declared as functions or arrow-function consts), and derives the URL
path from the route file's directory. Dynamic segment text such as
[id] and catch-alls such as [...slug] are preserved verbatim; route
groups (marketing) are dropped the way Next itself resolves them.

Detection keys on the next dependency in package.json or the presence
of route files in the tree. Same-file handlers resolve only when
unambiguous; missing or duplicate handlers stay unresolved. Pages
Router, route groups' URL composition, rewrites, middleware, and
server components stay out of scope. No identity, reconciliation,
schema, or drift-semantics changes.

Resolves mex-memory#95
- At most one route node per method per file. A route module can export
  each verb once, but TypeScript overloads and handlers left in block
  comments used to emit colliding node ids and fail the whole build
  (Graph staging invariant: duplicate node id).
- deriveRoutePath locates the App Router root as a path segment, not a
  substring: indexOf('app') turned apps/web/app/api/orders/route.ts
  into /s/web/app/api/orders. Segment matching fixes monorepos
  (apps/web, packages/webapp) with or without a src/ root.
- Typed const handlers (export const GET: RouteHandler = ...) now bind:
  the name may carry an annotation before the initializer.
- Private folders (_lib) are dropped from derived paths like route
  groups, matching Next's own routing.
- Edge label moves to nextjs-route-handler at confidence 0.8, matching
  Express's evidence class; detection comment now records why route
  files are an additional signal (root-only package.json in monorepos).
- The JavaScript fixture is valid JavaScript again.
- Changelog entry moved under [Unreleased] after the 0.8.1 release
  absorbed the old section.

Addresses review on mex-memory#179
@abhinav-phi
abhinav-phi force-pushed the feat/nextjs-app-router-resolver branch from a37a046 to 8bb517a Compare September 11, 2026 20:37
@abhinav-phi

Copy link
Copy Markdown
Contributor Author

All findings addressed.

Must fixes

  1. Duplicate ids — extraction now emits at most one route node per method per file (a module can export each verb once); the first declaration wins. Your overload snippet plus a block-commented stale handler is a test: exactly one GET /api/x node.
  2. Monorepo paths — the App Router root is located as a path segment (/(?:^|\/)src\/app(?:\/|$)/, then /(?:^|\/)app(?:\/|$)/), never a substring. apps/web/app/api/orders/route.ts/api/orders, packages/webapp/app/api/users/route.ts/api/users, apps/web/src/app/.../api/orders — all three in the test table now.

Should fix 3EXPORTED_ARROW allows an optional type annotation on the name: export const GET: RouteHandler = async () => ... binds (test included). The other forms (export const { GET } = handlers, withAuth(...) wrappers, export { x as GET }) are left for the follow-up you described — agreed emitting the route with an unresolved handler is the right shape there.

Smaller things — edge label is nextjs-route-handler at confidence 0.8 (integration query updated); private folders (_lib) are dropped from derived paths like route groups, with tests; the detection comment now records the monorepo reasoning for the route-file signal; nextjs-items-route.js is valid JavaScript; the changelog entry moved under a fresh ### Added in ## [Unreleased] after the 0.8.1 release absorbed the old section (rebase done, conflict resolved exactly that way).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Next.js] Add App Router route resolver

2 participants