Repository navigation
Description
Version
codebase-memory-mcp 0.9.0 (also measured on 0.9.1-rc.1 — see below)
Platform
Windows (x64)
Install channel
npm
Binary variant
standard
What happened, and what did you expect?
NestJS controllers produce no Route nodes at all. @Controller (class-level prefix) and @Get/@Post/@Patch/@Delete (method-level path + verb) are never composed into routes, so a NestJS service contributes zero server-side routes to the graph.
Expected: one Route node per handler, with the class prefix composed onto the method path (and, when present, the main.ts global prefix and URI version segment).
Actual: the Route label does not appear in get_graph_schema output for the project at all.
This is the same shape as the already-reported gaps for other decorator/annotation-routed frameworks — Java/Spring class-level @RequestMapping (#734), ASP.NET Core attribute routing (#1041), FastAPI router prefix (#678), Next.js App Router (#759) — so I am filing NestJS as its own data point rather than commenting on those. NestJS is the largest remaining decorator-routed server framework in the TS ecosystem, and #514 already notes NestJS DI defeats caller resolution, so NestJS support is partially on the radar already.
Consequence: cross-repo-intelligence matches Route ↔ Route, so for a NestJS-backend + Next.js-frontend fleet it can never produce an HTTP edge. On our 5-project store the pass reports projects_scanned: 5, total_cross_edges: 0 — and the empty result is indistinguishable from "there are genuinely no cross-service calls", which is the dangerous part for an agent consuming the graph.
Reproduction
Two throwaway projects, ~60 LOC total, no node_modules or build needed. All code below is written for this report — nothing proprietary.
1. nest-api/src/users/users.controller.ts
import { Controller, Get, Post, Patch, Delete, Param, Body } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller({ path: 'users', version: '1' }) // also fails with plain @Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get() findAll() { return this.usersService.findAll(); }
@Get(':id') findOne(@Param('id') id: string) { return this.usersService.findOne(id); }
@Post() create(@Body() dto: { name: string }) { return this.usersService.create(dto); }
@Patch(':id') update(@Param('id') id: string, @Body() dto: { name: string }) { return this.usersService.update(id, dto); }
@Delete(':id') remove(@Param('id') id: string) { return this.usersService.remove(id); }
}2. nest-api/src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get() check() { return { status: 'ok' }; }
}3. nest-api/src/main.ts — supplies the two app-level modifiers
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api', { exclude: ['health'] });
app.enableVersioning({ type: VersioningType.URI });
await app.listen(3000);
}
bootstrap();(plus a trivial app.module.ts wiring the two controllers, and a users.service.ts with the five methods)
4. web-client/src/api.ts — the caller side, for the cross-repo half
const BASE = 'http://localhost:3000';
export async function listUsers() { return (await fetch(`${BASE}/api/v1/users`)).json(); }
export async function getUser(id: string) { return (await fetch(`${BASE}/api/v1/users/${id}`)).json(); }
export async function createUser(name: string) {
return (await fetch(`${BASE}/api/v1/users`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }),
})).json();
}5. Index both and inspect the schema
$ echo '{"repo_path":"<repro>/nest-api","name":"nest-api"}' | cli index_repository
{… "nodes":39,"edges":66,"status":"indexed"}
$ echo '{"repo_path":"<repro>/web-client","name":"web-client"}' | cli index_repository
{… "nodes":9,"edges":8,"status":"indexed"}
$ echo '{"project":"nest-api"}' | cli get_graph_schema
# node_labels: Method(12), Decorator(7), File(5), Module(5), Class(4), Folder(3), Branch(1), Function(1), Project(1)
# → no "Route" label presentExpected 6 Route nodes:
| Verb | Effective path |
|---|---|
| GET | /api/v1/users |
| GET | /api/v1/users/:id |
| POST | /api/v1/users |
| PATCH | /api/v1/users/:id |
| DELETE | /api/v1/users/:id |
| GET | /health (excluded from the global prefix, unversioned) |
Actual: 0. Note Decorator = 7, so the decorators themselves are seen — they are just not turned into routes.
web-client likewise yields no Route label, so the client side of the pair is not extracted either.
Logs
Composition rules needed for NestJS, for reference:
- class prefix:
@Controller('users')or@Controller({ path: 'users', version: '1' }) - method verb + path:
@Get(':id'),@Post(),@Patch(':id'),@Delete(':id') - global prefix from
main.ts:app.setGlobalPrefix('api', { exclude: [...] }) - URI versioning from
main.ts:app.enableVersioning({ type: VersioningType.URI })
WhendefaultVersionis NOT set, versioning is per-controller: a controller that declares
version: '1'serves/api/v1/<path>, one that declares no version serves/api/<path>.
Both shapes coexist in the same app, so the version segment cannot be assumed globally.
Project scale (if relevant)
Fixture above: 39 nodes / 66 edges / 5 files. Also observed on a real NestJS 11 service in our fleet at 3,289 nodes / 7,669 edges, where 22 @Controller + 131 method decorators likewise yield no route for any real endpoint path.
I checked 0.9.1-rc.1 as well, since its notes mention "JAX-RS class+method @Path composition" and "Laravel Route::prefix()->group() routes" — the same composition problem. Installed it into an isolated cache and re-indexed that service: its Route count went 21 → 20, so the rc's route work does not cover NestJS. (Those 20–21 Route nodes are all literal-derived rather than decorator-derived — one is a bare http://localhost:3000 base URL, with file_path: "" and start_line: 0. That part looks like #1248 / #598 and I am not re-reporting it here.)
Confirmations
- I searched existing issues and this is not a duplicate.
- My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.
Activity
HungChu commentedon Aug 5, 2026
Sequencing note — please do not fix this before route-ownership resolution.
A finding in #1133 changes what "fixing this issue" would do. Investigating that thread, @junk151516 traced a set of 117 healthy-looking
CROSS_HTTP_CALLSedges and found they were path-string collisions: the matcher emits a cross-repo edge even when the route is defined inside the calling project itself, and nothing in the output distinguishes that from a call that genuinely leaves the service.That interacts with this issue in a way worth being explicit about:
Routenodes ⇒ no collisions ⇒total_cross_edges: 0. Useless, but visibly useless.The exposure is not exotic for NestJS specifically. The idiomatic layout is one
@Controller('<resource>')per resource under a shared global prefix, so any two NestJS services in one store that expose a same-named resource (/api/v1/users,/api/v1/tasks,/health) collide on path alone. Our own fleet is four NestJS services on a/api/v1/<resource>convention with overlapping resource vocabulary — it would start producing false edges the moment this extraction gap closes, unless ownership resolution lands first or alongside.So: happy to see this fixed, but the safe order is caller-project-preference first (@junk151516's preferred fix in #1133 — emit a cross-repo edge only when the path is not defined locally), then framework route extraction. If they ship in the other order, the composite behaviour is worse than the current bug, and harder to detect.
No change to the repro or measurements above.
DeusData commentedon Sep 25, 2026
Thank you so much, @HungChu. This is an exemplary report: a minimal, shareable repro, the exact composition rules, and the sequencing note about the caller-project preference, which really matters here.
The root cause was that the decorator route walk only understood Python-style decorator calls. A TypeScript decorator is a different node in the syntax tree, so
@Get(':id')was never looked at, and Nest's slash-less paths and the class-level@Controllerprefix weren't handled either. The fix in #2320 composes@Controller(string, array or{ path }form) with the method verb decorators into ordinary Route nodes with HANDLES edges, so your fixture goes from 0 to 6 routes (/users,/users/:id,/health, …).Following your note, the caller-local floor from #1459 (#2319) lands first, so cross-repo edges don't turn silently wrong. The global prefix and URI version from
main.tsare app-level, cross-file settings; we're tracking those separately rather than guessing at them.fix(extract): form Route nodes from NestJS @Controller/@get decorators (
Merge pull request #2320 from DeusData/fix/issue-1428
DeusData commentedon Oct 1, 2026
Thank you again for reporting this, @HungChu! The fix is now on
mainin f67243f (merged via #2320), and it will ship in the next release.junk151516 commentedon Oct 2, 2026
Verified f67243f on Windows: built from source with MSYS2/CLANG64, indexed into an isolated
CBM_CACHE_DIR+CBM_RUNTIME_DIR, with v0.11.0 as the control.The fix works for what it claims. The fixture from this issue goes from 0 to 6
Routenodes on f67243f (v0.11.0: 0), each with aHANDLESedge from the right method:GET/POST /usersGET/PATCH/DELETE /users/:idGET /healthExtra cases not in the PR's test, all behaving as the PR description says:
@Controller()+@Get('ping')→GET /ping@All('any')→ANY /any@Get('ghost')inside a class with no@Controller→ no route (nothing invented)@Controller(['items','products'])+@Get('list')→ only/items/list(first entry, as documented)A literal-URL client (
fetch('http://localhost:3000/users'),fetch('/health')) now gets 2CROSS_HTTP_CALLStonest-api(findAll,check); the same client against v0.11.0 gets 0.But the reporter's end-to-end case still yields
total_cross_edges: 0, for two reasons outside this fix:/users, and the app actually serves/api/v1/users. On the idiomatic Nest layout (a global prefix inmain.ts), a correct client call still won't match.fetch(`${BASE}/api/v1/users`)withconst BASE = 'http://localhost:3000'produces noRoute/HTTP_CALLSinweb-client. Even after I rewrote the calls to`${BASE}/users`so the path matches, there were still 0 cross edges. This looks like JS/TS: fetch/axios URLs built from template literals over module-level constants are not extracted as HTTP_CALLS #1147 (still open), not a gap in this fix.One small thing, possibly worth a follow-up: a trailing slash is kept verbatim.
@Put('/slash/:x/')→/slash/:x/(qualified name__route__PUT__/slash/{}/). Nest serves that as/slash/:x, so a caller to/slash/1may not match. I haven't tested the cross-repo side of this.Also, #2165 (open) describes the same
@Controller/@Getgap that this fix closes, so it may be a duplicate now.