Anti-Patterns
Recipes are working wiring examples with the sharp edges annotated. This page and the
anti-patternsrecipe served to coding agents by the MCP server share one source (@modularityjs/manifest), so they never drift apart.
LLMs often pattern-match from NestJS, Angular, or TypeORM and produce code that compiles but is semantically wrong here. Specifically watch for:
- ❌
@Module({ controllers: [HelloController] })— no such field. Valid@Modulekeys arename | imports | providers | preferences | pools | contracts | lateContracts | overrides. Register a controller viaproviders: [HelloController]andpools: [{ pool: HttpControllersPool, key: 'hello', useClass: HelloController }], plusimports: [HttpModule]. All three are required. - ❌
import 'reflect-metadata';at your app's entry point — unnecessary. The framework imports it as a side effect inside@modularityjs/diand@modularityjs/httpbefore any of your decorated classes evaluate. - ❌
"reflect-metadata"in your app'sdependenciesordevDependencies— comes transitively through@modularityjs/di(declared as a runtime dep there). Add it yourself only if you write decorators that callReflect.metadata/Reflect.getMetadatadirectly and want the ambient types —devDependencyin that case, neverdependency. - ❌
import { Container } from 'inversify';— never. Always@modularityjs/di. - ❌
import '@modularityjs/di-inversify';(side-effect form) — pass it:import { inversify } from '@modularityjs/di-inversify';thencreateApp({ di: inversify, ... }). - ❌
if (!container.isBound(X)) { ... }— don't use as a null-safety check. Declare the dependency in your module'simportsorproviders. Boot-time validation will catch missing contracts. - ❌ Putting a controller in only
providers: [...]and forgetting theHttpControllersPoolentry — it binds in DI but the HTTP driver doesn't see it, route registration is empty, requests 404. (Symptom:fastify.printRoutes()is empty.) - ❌
@Module({ services: [...], routes: [...], middlewares: [...] })— none of these exist. Useprovidersandpoolsper the schema above. - ❌ Building an
AppModulethat imports a driver (HttpFastifyModule) when you only need the contract — couples your app to a specific driver. ImportHttpModule, list the driver in the top-levelmodules: [...]array.
Two more, decorator-specific:
- ❌ Method decorators that don't fire (
@Get('/')records nothing, routes empty). Symptom:getMethodMetadataList(YourController)returns[]. Cause: the file is outsidetsconfig.includesoemitDecoratorMetadatais skipped. Fix: keep entry undersrc/(or whatever yourincludeglob matches), or extend the include. - ❌ Decorating a class with
@Controller()but never adding it to a module'spools: [{ pool: HttpControllersPool, ... }]— same 404 symptom as above. The decorator alone is necessary but not sufficient.
Auth + DB wiring traps (each cost an LLM agent multiple manifest lookups during validation):
- ❌ Guarding
@Auth()manually withif (!identity) return { error: ... }— that's a 200 with an error body, not a 401. Use@RequireAuth()from@modularityjs/http-authas a class- or method-level decorator; the adapter returns a proper 401 before your handler runs. For "must be authenticated AND have permission X", use@RequirePermission('X')from@modularityjs/http-authz— it implies@RequireAuth(). - ❌ Loading
HttpAuthModuleand expecting Bearer auth to work — the bridge ships no default authenticators. You must either load a bridge package (HttpAuthJwtModule,HttpAuthApiKeyModule,HttpAuthOidcModule,HttpAuthSessionModule,HttpAuthLocalModule) or contribute your ownHttpAuthenticatortoHttpAuthenticatorsPool. Without that,request.authis alwaysundefinedno matter whatAuthResolversPoolcontains. - ❌ Adding
HttpCsrfModuletomodules: [...]as a "hardening" step and expecting existing routes to keep working —HttpCsrfConfig.globalProtectiondefaults totrue, so every non-GET/HEAD/OPTIONScontroller route getsCsrfGuardattached atafterLoadand starts 403-ing without a valid token (an anonymousPOSTwith no session fails outright). This is the intended default; plan for it.@CsrfExempt()opts a single route out — use it for Bearer-token APIs and HMAC-verified webhooks, not to silence the guard. See the CSRF part of the session wiring section. - ❌
pools: [{ pool: DatabaseEntitiesPool, key: 'todo', useClass: Todo }]—DatabaseEntitiesPoolis a value pool. The driver iterates the pool's values and feeds them to TypeORM'sDataSource.entities. UsinguseClassputs a DI factory in the slot instead of the entity class; boot succeeds, but TypeORM never sees the entity,dataSource.getRepository(Todo)returns a repository against a table TypeORM never created, and migrations are generated against an empty schema. AlwaysuseValue: Todo.
Wrong layer for the wrap — three things that all "wrap something with next()" but at different scopes:
- ❌ Using a middleware (
@ApplyMiddleware) to transform a handler's return value — middleware runs before the handler exists; it has no access to what the handler will return. Use an interceptor (@UseInterceptor) — itsawait next()IS the handler's return. - ❌ Using an interceptor to trace every
DatabaseConnection.query()call — interceptors only wrap the HTTP handler, not the service calls made inside it. Use a plugin (@Plugin({ target: DatabaseConnection, method: 'query' })) — plugins wrap the actual method on the actual class, fire for HTTP and CLI and scheduled jobs. - ❌ Using a plugin to set an HTTP response header — plugins are at the DI layer; they see method args/return, not the HTTP request/response. Use middleware or an interceptor.
The shortcut: middleware's next() is the rest of the request pipeline; interceptor's next() is the handler; plugin's next() is the single method it targets. If your wrap needs to fire for non-HTTP code, it has to be a plugin.