Skip to content

feat(graph): add NestJS controller route resolver - #102

Open
abhinav-phi wants to merge 2 commits into
mex-memory:mainfrom
abhinav-phi:feat/nestjs-route-resolver
Open

feat(graph): add NestJS controller route resolver#102
abhinav-phi wants to merge 2 commits into
mex-memory:mainfrom
abhinav-phi:feat/nestjs-route-resolver

Conversation

@abhinav-phi

@abhinav-phi abhinav-phi commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What

  • Adds a NestJS FrameworkResolver for decorator-defined HTTP controller routes.
  • Detects NestJS through @nestjs/core or @nestjs/common dependencies.
  • Recognizes @Controller() prefixes and HTTP method decorators such as @Get(), @Post(), @Put(), @Patch(), @Delete(), @Options(), @Head(), and @All().
  • Emits stable route nodes and function_ref references to controller methods.
  • Resolves same-file handlers only when the target is unambiguous.
  • Adds a NestJS fixture, focused resolver tests, and framework-registry wiring.

Why

Closes #98.

This teaches the code graph about NestJS controller routing without changing the frozen FrameworkResolver interface or graph-core semantics.

Scope boundaries

This PR is limited to statically recognizable HTTP controller routes. Dependency-injection edges, guards, pipes, interceptors, middleware, gateways, GraphQL, microservices, and runtime decorator evaluation remain out of scope.

How to test

npm run typecheck
npm test
npm run build

Focused review should verify:

  • Positive and negative NestJS detection.
  • Controller-prefix and method-path normalization.
  • Multiple HTTP methods and parameterized paths.
  • Stable route-node identity.
  • Confident same-file handler resolution.
  • Missing or ambiguous handlers remain unresolved.
  • No graph identity, reconciliation, schema, or drift-semantics changes.

@theDakshJaitly
theDakshJaitly changed the base branch from code-graph-preview to main August 3, 2026 08:46
@abhinav-phi
abhinav-phi force-pushed the feat/nestjs-route-resolver branch from a652f92 to 99de109 Compare September 8, 2026 05:40
@abhinav-phi

Copy link
Copy Markdown
Contributor Author

Hi @theDakshJaitly @Yashasvi2229 — this PR has been open since mid-July with no reviews, so I gave it some maintenance:

  • Rebased onto latest main (it had fallen ~340 commits behind).
  • Verified locally: npm run typecheck, npm run build, and the resolver test suite all pass.
  • All CI checks on this branch are green.

The implementation targets the frozen FrameworkResolver interface, which is unchanged on current main, so no adaptation was needed beyond the rebase.

Is anything blocking this from review — or would you like changes first? Happy to iterate. (Tracking issue: #98, which this resolves.)

@theDakshJaitly

Copy link
Copy Markdown
Collaborator

Yeah our bad, got caught up in the new release, sorry for the delay in review

We will review asap, thanks for the rebase as well

@abhinav-phi
abhinav-phi force-pushed the feat/nestjs-route-resolver branch from 99de109 to 972c49c Compare September 9, 2026 21:37
@theyashasvipandey

Copy link
Copy Markdown
Collaborator

Thanks for this one @abhinav-phi, and for the rebase. The resolver is additive, detection is solid, and the happy path works end to end: I ran real rebuildGraph builds over a small NestJS project and every route linked to the right method (GET /users/:id → UsersController::findOne), including same-named methods in different controller files. The resolver suites pass too.

I then probed the extractor against the decorator forms real NestJS code uses. Three things need fixing before merge.

Must fix

1. Two routes with the same method + path fail the whole graph build

The route id is generateNodeId(filePath, "route", routeName), so two routes with the same name in one file get the same id, and the engine rejects duplicate ids. The whole build throws, not just this file. NestJS's own versioning pattern triggers it:

@Controller('users')
export class UsersController {
  @Version('1') @Get() findAllV1() {}
  @Version('2') @Get() findAllV2() {}
}
Graph staging invariant failed: duplicate node id route:9fe09bb0999fbf19a48312a2d9fa2d6e.

@Get() + @Get('/') in one controller does the same, and so does an old handler left in a block comment. The Express resolver avoids this by putting the handler in the signature (GET /users -> findAllV1) and adding an ordinal to the role. Doing the same here fixes it. Please add the @Version case as a test.

2. Non-string @Controller arguments give wrong paths

DECORATOR_REGEX only matches @Controller() with a string or no argument. Any other form is skipped entirely, so the prefix is either lost or inherited from the previous controller in the file:

source derived expected
@Controller({ path: 'users', version: '1' }) + @Get(':id') GET /:id GET /users/:id
same, after an @Controller('admin') class in the file GET /admin/:id GET /users/:id
@Controller(USERS_PATH) GET /:id unknown
@Controller(['users', 'people']) + @Get() GET / GET /users, GET /people

The object form is common (it's how controller versioning and host routing are declared), so please parse { path: '…' }. For the forms that can't be read statically (a constant, an array), skip that controller's routes rather than emitting a wrong path. Either way, every @Controller(...) should reset the prefix so nothing carries over.

3. Commented-out decorators create phantom routes

The regex runs over the raw file text, comments included:

@Controller('users')
export class UsersController {
  // @Get('legacy')
  @Get(':id')
  findOne() {}
}

This produces both GET /users/:id and a phantom GET /users/legacy, both pointing at findOne. Blanking comments out before scanning (with spaces, so offsets and line numbers stay correct) fixes this and the block-comment case in #1.

Should fix

4. Two controllers in one file lose their links

Resolution matches the handler by name within the file, so two controllers in one file that both have findAll leave both routes unresolved. I confirmed this in a real build. #98 asks to prefer the class context. The TypeScript extractor already names methods UsersController::findAll, so tracking the class that follows each @Controller and matching on qualifiedName resolves both.

Smaller things

  • Parenthesis skipping isn't string-aware. @ApiOperation({ summary: 'List users :)' }) between @Get() and the method silently drops the route. Rare, but it fails without any signal.
  • Array method paths. @Get(['list', 'all']) produces no route.
  • Integration test. The unit tests use a fake context. One test through rebuildGraph (like the Next.js resolver's in feat(graph): add Next.js App Router route resolver #179) would have caught 1 and 4.
  • Edge label. resolvedBy: "framework" / confidence 1 doesn't identify the resolver; Express uses express-route-handler / 0.8 for the same evidence. Something like nestjs-route-handler would match.
  • Fixture and header comments. The fixture has leftover notes ("Missing handler name (anonymous function)… let's just make it a normal one"), and the file header mentions a single-controller-per-file assumption the code doesn't make. Worth tidying.
  • CHANGELOG. Please add an entry under ## [Unreleased]### Added, as feat(graph): add Next.js App Router route resolver #179 does.

Review-driven rework of the NestJS route resolver, addressing three
must-fix findings from real-build probing:

- Duplicate node ids killed whole builds: NestJS versioning
  (@Version('1') @get() findAllV1 / @Version('2') @get() findAllV2)
  emits two routes with the same method+path. The handler now rides in
  the signature (GET /users -> findAllV1) and an ordinal joins the
  role, mirroring Express, so same-named routes keep distinct ids.
- @controller arguments beyond string literals were skipped entirely,
  so the prefix was lost or inherited from the previous controller.
  Arguments are now read string-aware (a ')' inside 'List users :)'
  no longer breaks anything): string form, object form ({ path: ... }),
  and empty are handled; constants and arrays skip that controller's
  routes rather than guessing. Every @controller resets the prefix and
  binds to the next class, so two controllers in one file each keep
  their own prefix.
- Commented-out decorators invented phantom routes. Comments are
  blanked with spaces before scanning — offsets and line numbers
  survive, comments stop being code.

Resolution now records the owning controller class in the reference
candidates and prefers a same-file qualified-name match, so two
controllers that both declare findAll no longer leave both routes
unresolved. Edge label moves to nestjs-route-handler at confidence
0.8, matching Express's evidence class. Fixture notes tidied; a
rebuildGraph integration test covers the versioned-route and
comment-decorator cases end to end.

Addresses review on mex-memory#102
@abhinav-phi
abhinav-phi force-pushed the feat/nestjs-route-resolver branch from 972c49c to ad61598 Compare September 11, 2026 20:30
@abhinav-phi

Copy link
Copy Markdown
Contributor Author

All findings addressed — thanks for probing the decorator forms; every one of these reproduced locally.

Must fixes

  1. Duplicate ids — the handler now rides in the signature (GET /users -> findAllV1) and an ordinal joins the role (nestjs-route:<n>), mirroring Express. The @Version('1')/@Version('2') case is in the fixture and asserted: three GET /users nodes, three distinct ids. Comment-blanking (must-fix 3) removes the block-comment variant entirely.
  2. Non-string @Controller argumentsextractBalancedArgs (string-aware) now reads the full argument text: empty → no prefix; string literal → prefix; object form → path property ({ path: 'users', version: '1' }GET /users/:id); constants and arrays → that controller's routes are skipped rather than guessed, per your instruction. Every @Controller(...) resets the prefix and binds to the next class declaration, so nothing carries over between controllers.
  3. Commented decorators — comments are blanked with spaces before scanning (state machine over line/block comments), so offsets and line numbers survive while // @Get('legacy') and block-comment handlers stop being code. Tested.

Should fix 4 — the reference now carries candidates: [::] from extraction; resolution prefers a same-file qualified-name match when the simple name is ambiguous. Two controllers with findAll in one file both resolve (test included).

Smaller things — paren skipping is string-aware (@ApiOperation({ summary: 'List users :)' }) test added); @Get([...]) non-literals skip (consistent with the controller rule); an end-to-end rebuildGraph test now covers versioned routes + commented decorators; edge label is nestjs-route-handler at confidence 0.8 with the integration query updated; fixture notes and the stale header comment are gone; CHANGELOG entry added under ## [Unreleased]### Added on this branch.

The one deliberate non-change: @Get(['list', 'all']) emits nothing (array paths skip like unreadable controller args) — happy to fan those out instead if you'd rather.

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.

[NestJS] Add controller route resolver

3 participants