Node API

Types ship with the package. diagnose() returns the same result the CLI formats:

import { diagnose, diagnoseMonorepo } from "nestjs-doctor";
 
const result = await diagnose("./my-nestjs-app");
result.score; // { value: 82, label: "Good" }
result.diagnostics; // Diagnostic[]
result.summary; // { total, errors, warnings, info, byCategory }
 
const mono = await diagnoseMonorepo("./my-monorepo");
mono.isMonorepo; // true
mono.subProjects; // [{ name: "api", result }, ...]
mono.combined; // Merged DiagnoseResult

DiagnoseResult carries score, diagnostics, summary, project, ruleErrors, elapsedMs, and schema when an ORM was detected.

Incremental scanning

Editors and language servers prepare the context once, then re-scan single files as they change instead of calling diagnose() again:

import {
  prepareAnalysis,
  updateFile,
  checkFile,
  checkAllFiles,
  checkProject,
} from "nestjs-doctor";
 
const { context, customRuleWarnings } = await prepareAnalysis("./my-nestjs-app");
 
updateFile(context, "/absolute/path/to/changed-file.ts");
 
const { diagnostics, errors } = checkFile(context, "/absolute/path/to/changed-file.ts");
const allFileResults = checkAllFiles(context);
const projectResults = checkProject(context);

prepareAnalysis parses every file, builds the module graph, and resolves the providers. updateFile re-parses one file and refreshes the module graph, the providers, and the endpoint and schema graphs.

checkFile runs the file-scoped rules on that one file, and checkAllFiles runs them across every file. checkProject runs the project-scoped ones.

Granular updates

Refresh the module graph or the provider map on their own, without going through updateFile:

import { updateModuleGraphForFile, updateProvidersForFile } from "nestjs-doctor";
 
updateModuleGraphForFile(
  context.moduleGraph,
  context.astProject,
  filePath,
  context.pathAliases,
);
updateProvidersForFile(context.providers, context.astProject, filePath);

Both return void and mutate the object handed to them. That object is the one the context holds, so a later checkProject(context) reads the updated graph.

Pass context.pathAliases. It defaults to an empty map, and updateProvidersForFile has no such parameter. Omit it and the file's aliased imports stop resolving, so that module's edges vanish with no error.

getRules() is exported alongside them and returns every built-in rule as AnyRule[].

AnalysisContext

The prepareAnalysis function returns an AnalysisContext that holds:

FieldTypeDescription
astProjectProjectts-morph project with all source files
configNestjsDoctorConfigResolved configuration
filesstring[]Collected file paths
fileRulesRule[]File-scoped rules (filtered by config)
projectRulesProjectRule[]Project-scoped rules (filtered by config)
moduleGraphModuleGraphImport/export graph
endpointGraphEndpointGraphTraced HTTP routes and the dependencies each one pulls
providersMap<string, ProviderInfo>Resolved NestJS providers
guardDecoratorsGuardDecoratorIndexPer file, the names of the wrapper decorators that apply @UseGuards()
pathAliasesPathAliasMaptsconfig paths aliases, each mapped to its absolute targets
projectProjectInfoDetected project metadata (name, version, ORM, framework)
schemaGraphSchemaGraph | undefinedExtracted database schema (if ORM detected)
schemaRulesSchemaRule[]Schema-scoped rules (filtered by config)
targetPathstringRoot directory of the scanned project
installRootstring | undefinedWhere to resolve node_modules from, when that is not targetPath

Code graph

A second structure beside the endpoint graph. It holds one node per declared method of every Nest class, and one edge per call site. The endpoint graph nests a subtree under each route, so one method that five routes reach appears in five subtrees.

Build one from a prepared context:

import { prepareAnalysis, codeGraphFor } from "nestjs-doctor";
 
const { context } = await prepareAnalysis("./my-nestjs-app");
const graph = codeGraphFor(context);
 
graph.nodes; // MethodNode[]
graph.edges; // CallEdge[]
graph.entries; // EntryPoint[], the routes whose handler became a node

codeGraphFor costs a pass over every indexed method, so it builds on the first call and caches against the context. updateFile drops that cache. Nothing on the diagnostics path calls it, so neither diagnose() nor checkFile builds it.

The endpoint graph stops at 5000 dependency nodes and marks the endpoint truncated. The code graph has no such limit.

Node ids

A node id is ${path}::${ClassName}#${methodName}. The path is the absolute path of the declaring file, the same one the endpoint graph reports. An installed package has no file to point at, so its import specifier fills the slot, and a receiver that resolves to neither leaves it empty. Five variations sit inside that shape:

Node idMeans
/app/src/orders.service.ts::OrdersService#findAn instance method
/app/src/token.service.ts::TokenService.formatA static, separated with .
/app/src/helpers.ts::#formatLabelA free function, with the class left empty
/app/src/prisma.service.ts::PrismaService#user.findUniqueA member call, from this.prisma.user.findUnique()
@nestjs/common::Logger#errorAn installed package, whose import specifier fills the path slot
::{ tick(): void }#tickA receiver that resolved to neither, leaving the path empty

The parser hides node_modules, so a package has no declaration to key on. The import specifier is what keeps two packages exporting one name apart.

Body and order

Each node carries an ordered body of the parts that are not calls: returns, throws, and plain steps. Body items and the node's outgoing edges share one dense 0..n-1 sequence, so order interleaves them back into the method as written.

Every edge says where its call site sits in the control flow:

FieldSays
conditional, conditionPathWhether the call runs on a condition, and every enclosing construct, outermost first
branchKind, branchGroupIdWhich arm of a branch holds it, and which arms are mutually exclusive
tryRegionThe try covering it, matching the branchGroupId of the catch that handles it
awaitedWhether the code awaits the call site itself
iterationKind, iterationLabelWhether it repeats, and the construct that repeats it
guardThrowThe exception thrown when the returned value fails an immediate null check
assignedTo, commentThe variable taking the result, and the comment above the call

A call handed to Promise.all has awaited: false. Its iterationKind is concurrent, which is what records the collection.

Reading a graph

Four helpers read a built graph:

FunctionReturns
nodeId(filePath, className, methodName, member?)The id a node would carry
indexNodes(graph)Map<NodeId, MethodNode>
outgoing(graph)Map<NodeId, CallEdge[]>, edges grouped by their from
reachableFrom(graph, entryIds)Set<NodeId>, the entry ids included

Walk every method one route reaches:

import { codeGraphFor, reachableFrom, indexNodes } from "nestjs-doctor";
 
const graph = codeGraphFor(context);
const byId = indexNodes(graph);
const entry = graph.entries.find((e) => e.routePath === "/orders");
 
for (const id of reachableFrom(graph, entry ? [entry.node] : [])) {
  console.log(byId.get(id)?.kind);
}

reachableFrom follows edges without tracking depth, so a recursive or cyclic call chain terminates instead of repeating.

Building and merging

codeGraphFor wraps buildCodeGraph, which takes the four inputs directly:

import { buildCodeGraph, mergeCodeGraphs } from "nestjs-doctor";
 
const graph = buildCodeGraph(
  context.astProject,
  context.files,
  context.providers,
  context.endpointGraph.endpoints,
);
 
const combined = mergeCodeGraphs([graph, otherGraph]);

Pass all four. buildCodeGraph has no defaults, and codeGraphFor is the shorter route whenever a context is already in hand.

mergeCodeGraphs is what a monorepo scan uses, since each sub-project builds its own graph, and the scan drops each context before building the next. The result does not depend on the order the graphs arrive in.

Encoding

encodeCodeGraph turns file paths and node ids into indices and drops every field sitting at its default. decodeCodeGraph restores the graph exactly.

Round-trip a graph through the compact form:

import { encodeCodeGraph, decodeCodeGraph } from "nestjs-doctor";
 
const encoded = encodeCodeGraph(graph);
const restored = decodeCodeGraph(encoded);

The report artifact carries that encoded form in its optional codeGraph field. A tool reading nestjs-doctor-report.json decodes it rather than rebuilding it. See Output.