GUIDELINESNESTTRPC.md
July 6, 2026 · View on GitHub
Core Philosophy – This library MUST feel native in NestJS projects
Every single decision must follow NestJS philosophy exactly as @nestjs/graphql and @nestjs/websockets do.
1. Overall Architecture Assumptions (never break these)
- It is a first-class NestJS integration package, not a thin wrapper around tRPC.
- Everything must be decorator-first, OOP, and heavily use NestJS DI.
- Mirror the exact DX of @nestjs/graphql (code-first approach with autoSchemaFile).
- Current stabilization support line:
- Node.js
>=20 - NestJS
11.x - tRPC
11.x
- Node.js
- Full integration with NestJS enhancer pipeline is NON-NEGOTIABLE:
- @UseGuards, @UseInterceptors, @UsePipes, @UseFilters must work exactly as on @Controller or @Resolver.
- Request-scoped providers, async providers, and REQUEST injection must work.
- Adapter-agnostic: zero code changes needed when switching between Express and Fastify.
- Support BOTH validation worlds without forcing one:
- Zod (for tRPC type inference and frontend magic)
- class-validator + ValidationPipe + DTOs (for classic NestJS users)
2. Public API Assumptions (this is what users will copy-paste)
- Primary onboarding API:
- TrpcModule.forRoot({ path, autoSchemaFile, createContext? })
- Decorators (exactly these names and signatures):
- @Router('namespace'?) // like @Controller or @Resolver
- @Query('name', { input?, output? })
- @Mutation('name', { input?, output? })
- @Subscription('name', { input?, output? })
- @TrpcContext('key') // for context injection
- @Input() // optional helper for Zod schemas
- Generated AppRouter type must be importable and fully typed for the client.
- Advanced supported testing API:
TrpcRoutermay remain public when it is part of the official in-process testing story viagetRouter().createCaller(...).TrpcRoutermust stay out of installation and quick-start docs; it belongs in testing-focused guidance only.- If we ever want
TrpcRouterto become private, we must first introduce an equally strong official testing abstraction and migrate docs, samples, and tests to it.
3. Sample Folder Rules
sample/00-showcasemust demonstrate:- Feature modules with routers + services
- Constructor DI (including request-scoped services)
- Guards, interceptors, pipes, and filters on procedures
- Mixed validation (Zod + class-validator DTO in the same router)
- Context injection via @TrpcContext
- Express + Fastify mains
- Client with full type safety + typecheck step
- Focused samples under
sample/01-*,sample/02-*, etc. should isolate one topic with minimal noise. - Never simplify the full showcase for brevity — richness proves the integration depth.
4. Implementation Rules
- All routers are plain classes with decorators (no manual createRouter() calls visible to user).
- Use NestJS metadata reflection and discovery exactly like GraphQL module.
- Exception mapping: BadRequestException → tRPC BAD_REQUEST, etc.
- Context creation must be injectable and support both sync and async.
- Schema generation must produce a clean AppRouter type that works with createTRPCProxyClient.
- Never expose tRPC internals to the user unless they opt-in via advanced config.
- Unsupported internal surface includes:
- deep imports into package internals such as
@nest-native/trpc/dist/... - raw context/runtime helpers
- raw schema generator helpers
- transport internals such as
TrpcHttpAdapter - metadata constants and DI tokens intended for package internals
- deep imports into package internals such as
- These internals must not appear in root exports, installation docs, or quick-start examples.
- Keep the package lean — no unnecessary dependencies in the published library package.
- Sample-only dependencies are acceptable when they prove an integration scenario; they must not leak into the published package contract.
5. Non-Negotiable Style & Patterns
- Use NestJS naming conventions (@nestjs/common style).
- Prefer constructor injection over module-level providers when possible.
- Always support global, module, and method-level enhancers.
- Tests must cover enhancer pipeline, request scoping, and both adapters.
- Documentation and README should follow Nest-style clarity without claiming official status.
- Documentation must preserve API tiers:
- onboarding docs center on
TrpcModule, decorators, and generatedAppRouter TrpcRouterappears only in testing-oriented docs- unsupported internals are documented as unsupported, not as hidden power-user APIs
- onboarding docs center on
6. When in doubt
- Ask yourself: “Would this feel natural in a @nestjs/graphql project?”
- If the answer is no → redesign it until the answer is yes.
Follow these assumptions in EVERY file you generate or modify. This is not a suggestion — it is the project constitution.
7. Differentiation Strategy
- NEVER use @UseMiddlewares — always expose real @UseGuards / @UsePipes / etc.
- Support both Zod AND class-validator in the same router.
- Prefer autoSchemaFile over external CLI.
- Always include @TrpcContext decorator.
8. Security Review Requirements (MANDATORY)
- Every PR analysis must include an explicit security pass; security is never implicit.
- Supply-chain checks are NON-NEGOTIABLE:
- Review every dependency addition/update (including lockfile changes) for legitimacy and necessity.
packages/trpc/package.jsonmust keep an explicit empty"dependencies": {}block.- Runtime requirements belong in
peerDependencies; package-local test/build tools belong indevDependencies. - Flag unpinned Git/URL dependencies unless there is explicit, documented approval.
- Inspect install/lifecycle scripts (
preinstall,install,postinstall,prepare) for malicious behavior risk. - Watch for typosquatting, suspicious package ownership changes, abandoned packages, and sudden lockfile churn.
- Treat Dependabot PRs as automation assistance, not approval to merge:
- Keep major updates isolated unless a maintainer explicitly requests grouping.
- Verify
packages/trpc/package.jsonstill has"dependencies": {}after every dependency PR. - Run the same validation expected for a human-authored dependency update.
- Do not auto-accept lockfile churn without understanding which direct dependency caused it.
- Strictness scope. The non-negotiables (100% coverage, cognitive-complexity ≤ 15, zero published runtime deps, isolated major-version review) govern the core published package (
packages/trpc). Non-core code —sample/*(including the Angular showcase), thewebsite/, and dev tooling — uses lighter rules: their dependency updates (including majors) may merge on green CI without the core's major-isolation ceremony. - Audit scope. The
security:auditrelease gate audits the published surface —npm audit --omit=dev --audit-level=high. Since the package publishes"dependencies": {}, this is exactly what consumers install. Advisories confined to dev/peer/build tooling or the docswebsite/are tracked and patched via Dependabot but do not block releases — they cannot reach consumers. Patch them in their own PRs. - Application security checks are required in PR reviews:
- Auth/authz bypass risk, privilege escalation, and data exposure in handlers, decorators, and context wiring.
- Input-validation gaps and injection surfaces (command/template/SQL-like patterns, prototype pollution, path traversal).
- Unsafe dynamic execution (
eval,new Function) and unsafe deserialization patterns. - Secret leakage in code, tests, sample files, logs, and documentation.
- Transport risks such as insecure CORS/CSRF assumptions, SSRF vectors, and open redirects when relevant.
- Any unresolved high-risk security finding blocks merge until mitigated or explicitly accepted by maintainers.
9. Release Version Synchronization (MANDATORY)
- Version drift between
packages/trpcandsample/*is a release blocker. - When bumping
packages/trpc/package.jsonversion, update ALLsample/*/package.jsonentries for"@nest-native/trpc"in the same change. - Regenerate
package-lock.jsonafter version alignment (npm install) so resolution state is consistent. - Never publish if any sample still points to an older package version.
npm run release:checkis a release blocker:- sample version sync must pass
- README link validation must pass
- workspace resolution must show the target version everywhere
- package tarball validation must pass and exclude unintended artifacts such as
.tsbuildinfo
Required pre-publish checklist:
- Bump version in
packages/trpc/package.json. - Update
sample/*/package.jsonto the exact same@nest-native/trpcversion. - Run
npm install. - Run
npm run release:check. - Run
npm ls @nest-native/trpc --workspaces --depth=0and verify every sample resolves to the target version. - Run full validation:
npm run ci.
Required post-publish checklist:
- Tag the release commit on
main:git tag v<version> <release-commit-sha> && git push origin v<version>. Use a lightweight tag namedv<version>(e.g.v0.4.3) pointing at thechore: release v<version>commit. Tag pushes bypassmain's branch-protection rules and do not require a PR. - Confirm registry version exists:
npm view @nest-native/trpc@<version> version. - Download published artifact:
npm pack @nest-native/trpc@<version>. - Re-run
npm run ciwith samples pinned to that published version before closing the release.
10. Cognitive Complexity Review
- When changes touch
packages/trpc/**/*.ts, AI agents should runnpm run complexity:checkandnpm run complexity:report. - CI enforces SonarJS' default cognitive-complexity threshold of
15per package source function. - Treat the PR complexity report as a review signal for deltas and hotspots, not an automatic refactor mandate.
- Do not reduce complexity by weakening Nest-native architecture, public API clarity, validation behavior, or test coverage.
11. Mutation testing (Stryker — occasional targeted audit, local only, never in CI)
Mutation testing here is an occasional, targeted audit — not a per-PR gate.
Run it deliberately when you've written or reworked non-trivial logic in a file
and want to know whether its tests actually pin the behavior. It has found real
gaps here (a param-metadata propertyKey guard, redundant serializer branches),
so it is worth doing — occasionally and precisely. Plain npm test and CI are
unchanged; CI never runs mutation testing.
Run it scoped, never full-package. The command runner re-runs the whole suite per mutant, so a full run is slow and a large surface can time out. Scope to the one file you changed:
STRYKER_MUTATE='packages/trpc/generators/zod-serializer.ts' npx stryker run --concurrency 2— low concurrency keeps RAM in check and avoids the CPU oversubscription that turns kills into timeouts. Read survivors fromreports/stryker-incremental.jsonorreports/mutation/mutation.html.npm run test:mutation/test:mutation:fullremain for incremental / full passes; expect them to be slow on a large surface.
Verify a kill without re-running Stryker — the fast path. Hand-apply the
surviving mutation to the source, run the plain suite (or just the one spec),
confirm your new test fails, then git checkout -- to revert. This decouples
the slow "find survivors" step from a fast "prove the kill" step.
If a run times out, kill the leftovers first — a killed Stryker command can
leave detached test processes that starve the next run; pgrep -f stryker,
kill -9, confirm RAM recovered, then retry.
Treat each survivor by the doctrine: add a test that kills it; simplify
redundant code whose mutant is behaviorally equivalent (with a CHANGELOG note);
mark a genuine equivalent with // Stryker disable next-line <Mutator>: <reason>; or, for timing/randomness, assert bounds/progression. Keep CI fast —
that is a deliberate contract.