Correctness rules
20 rules that detect bugs, missing decorators, and runtime errors.
| Rule | Severity | What it catches |
|---|---|---|
no-missing-injectable | error | Provider with constructor deps missing @Injectable() |
no-duplicate-routes | error | Same method + path twice in a controller |
no-missing-guard-method | error | Class named *Guard missing canActivate() |
no-missing-pipe-method | error | Class named *Pipe missing transform() |
no-missing-filter-catch | error | @Catch() class missing catch() |
no-missing-interceptor-method | error | Class named *Interceptor missing intercept() |
require-inject-decorator | error | Untyped constructor param without @Inject() |
param-decorator-matches-route | error | @Param() name doesn't match any :param in the route path |
factory-inject-matches-params | error | useFactory inject array length mismatches factory parameter count |
prefer-readonly-injection | warning | Constructor DI params missing readonly |
require-lifecycle-interface | warning | Lifecycle method without interface |
no-async-without-await | warning | Async function with no await |
no-duplicate-module-metadata | warning | Duplicate entries in @Module() arrays |
no-missing-module-decorator | warning | Class named *Module without @Module() |
no-fire-and-forget-async | warning | Async call without await in non-handler methods |
no-duplicate-decorators | warning | Same decorator appears twice on a single target |
validated-non-primitive-needs-type | warning | Non-primitive DTO property with class-validator decorators missing @Type() |
validate-nested-array-each | warning | @ValidateNested() on array property missing { each: true } |
no-empty-handlers | info | HTTP handler with empty body |
injectable-must-be-provided | info | @Injectable() class not registered in any module's providers |
no-missing-injectable
Scope: Project
Detects classes listed in a module's providers array that declare constructor dependencies but are missing the @Injectable() decorator.
Why: Without @Injectable(), NestJS cannot resolve constructor dependencies, causing runtime errors.
// user.service.ts
export class UserService { // Missing @Injectable()
constructor(private readonly db: DatabaseService) {}
}
// user.module.ts
@Module({ providers: [UserService] })
export class UserModule {}no-duplicate-routes
Detects two handlers with the same HTTP method, path, and version in a single controller.
Why: Duplicate routes produce undefined behavior. Only one handler runs, silently shadowing the other.
@Controller('users')
export class UserController {
@Get(':id')
findOne(@Param('id') id: string) { /* ... */ }
@Get(':id') // duplicate route
getUser(@Param('id') id: string) { /* ... */ }
}no-missing-guard-method
Detects classes named *Guard that carry @Injectable() and are missing the canActivate() method.
Why: A guard without canActivate() will cause a runtime error when NestJS tries to invoke it.
Note: Three gates decide whether a class is checked. The name must end in Guard, the class must carry @Injectable(), and it must have no extends clause.
An implements CanActivate clause is never read, so a class named Authorizer is not checked whatever it implements. A guard that extends a base class is skipped too, because canActivate() can come from the parent, as in extends AuthGuard('jwt').
@Injectable()
export class AuthGuard implements CanActivate {
// missing canActivate()
}no-missing-pipe-method
Detects classes named *Pipe that carry @Injectable() and are missing the transform() method.
Why: Pipes must implement transform() or NestJS will throw at runtime.
Note: The same three gates as no-missing-guard-method apply: the Pipe name suffix, @Injectable(), and no extends clause. An implements PipeTransform clause is not read.
@Injectable()
export class ParseIntPipe implements PipeTransform {
// missing transform()
}no-missing-filter-catch
Detects exception filter classes (decorated with @Catch()) missing the catch() method.
Why: Filters must implement catch() or NestJS will throw at runtime.
@Catch(HttpException)
export class HttpFilter implements ExceptionFilter {
// missing catch()
}no-missing-interceptor-method
Detects classes named *Interceptor that carry @Injectable() and are missing the intercept() method.
Why: Interceptors must implement intercept() or NestJS will throw at runtime.
Note: The same three gates as no-missing-guard-method apply: the Interceptor name suffix, @Injectable(), and no extends clause. An implements NestInterceptor clause is not read.
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
// missing intercept()
}require-inject-decorator
Detects constructor parameters without a type annotation that are also missing @Inject().
Why: Without a type or @Inject(), NestJS cannot resolve the dependency. This causes a runtime error.
@Injectable()
export class UserService {
constructor(private readonly connection) {} // No type, no @Inject()
}param-decorator-matches-route
Detects @Param() decorator names that don't match any :param segment in the route path (including the controller prefix).
Why: A mismatched @Param() name silently receives undefined at runtime, leading to bugs that are hard to trace.
@Controller('users')
export class UserController {
@Get(':id')
findOne(@Param('userId') userId: string) { // 'userId' doesn't match ':id'
return this.userService.findOne(userId);
}
}factory-inject-matches-params
Detects useFactory providers where the inject array length doesn't match the factory function's parameter count.
Why: NestJS maps inject tokens to factory parameters by position. A count mismatch means the factory receives wrong or missing values, causing silent bugs or runtime errors.
@Module({
providers: [
{
provide: 'CONFIG',
useFactory: (config) => config.get('db'), // 1 param
inject: [ConfigService, LoggerService], // 2 tokens
},
],
})
export class AppModule {}prefer-readonly-injection
Detects constructor DI parameters missing the readonly modifier.
Why: Injected dependencies should not be reassigned. The readonly modifier prevents accidental mutation and communicates intent.
Note: Report-only. It is listed in full and counts toward neither the score nor --blocking, because it is a house style rather than a defect. See Surfaces.
@Injectable()
export class UserService {
constructor(private db: DatabaseService) {}
}require-lifecycle-interface
Detects lifecycle methods (onModuleInit, onModuleDestroy, etc.) without the corresponding interface.
Why: Without the interface, TypeScript will not catch typos in the method name, and the intent is unclear to readers.
@Injectable()
export class AppService {
onModuleInit() { // No OnModuleInit interface
// ...
}
}no-async-without-await
Detects async functions or methods that never use await.
Why: An async function without await creates an unnecessary promise wrapper. Either remove async or add the missing await.
Framework entry points are exempt, because Nest awaits what they return. A method carrying one of these decorators is never reported:
| Kind | Decorators |
|---|---|
| HTTP routes | @Get, @Post, @Put, @Patch, @Delete, @Head, @Options, @All |
| GraphQL | @Query, @Mutation, @Subscription, @ResolveField, @ResolveProperty, @ResolveReference |
| Microservices | @MessagePattern, @EventPattern, @GrpcMethod, @GrpcStreamMethod |
| WebSockets | @SubscribeMessage |
| ts-rest | @TsRestHandler |
Note: Report-only. It is listed in full and counts toward neither the score nor --blocking, because it is a house style rather than a defect. See Surfaces.
@Injectable()
export class UserService {
async findAll() {
return this.users; // No await needed
}
}no-duplicate-module-metadata
Detects duplicate entries in @Module() metadata arrays (imports, providers, controllers, exports).
Why: Duplicate entries are redundant and suggest a copy-paste error. NestJS ignores duplicates, but they obscure the intended module composition.
@Module({
providers: [UserService, AuthService, UserService], // duplicate entry
})
export class UserModule {}no-missing-module-decorator
Detects classes named *Module that are missing the @Module() decorator.
Why: A class following the module naming convention without @Module() is likely missing the decorator, which means NestJS will not register it.
export class UserModule { // Missing @Module()
// ...
}no-fire-and-forget-async
Detects async function calls without await in non-handler methods (services, etc.).
Why: Calling an async function without await produces a floating promise. If the promise rejects, the error is silently discarded, leading to data loss or inconsistent state.
@Injectable()
export class OrderService {
complete(orderId: string) {
this.notificationService.sendEmail(orderId); // no await
this.analyticsService.track('order_completed'); // no await
}
}no-duplicate-decorators
Detects the same decorator appearing twice on a single class, method, property, or constructor parameter.
Why: A duplicate decorator is almost always a copy-paste error. The second occurrence silently overwrites the first or has no effect.
Note: Most decorators are compared by their full text. @ApiResponse({ status: 200 }) and @ApiResponse({ status: 404 }) are two decorators, but writing either one twice verbatim is still reported.
A target can carry only one @Controller, @Injectable, @Module, @Catch, @Entity, @Global, @Resolver, @WebSocketGateway, or route decorator. For those the second occurrence is reported even when its arguments differ.
@Controller('users')
export class UserController {
@Get(':id')
@Get(':id') // duplicate decorator
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
}validated-non-primitive-needs-type
Detects DTO properties with class-validator decorators on non-primitive types that are missing @Type() from class-transformer.
Why: class-transformer needs @Type() to know which class to instantiate for nested objects. Without it, validation runs against a plain object and nested rules are silently skipped.
export class CreateOrderDto {
@ValidateNested()
address: AddressDto; // missing @Type()
}validate-nested-array-each
Detects @ValidateNested() on array-typed properties that are missing { each: true }.
Why: Without { each: true }, @ValidateNested() validates the array object itself instead of each element, so invalid items pass validation silently.
export class CreateOrderDto {
@ValidateNested() // missing { each: true }
@Type(() => ItemDto)
items: ItemDto[];
}no-empty-handlers
Detects HTTP handler methods with empty bodies.
Why: An empty handler returns undefined, which NestJS serializes as an empty response. This is nearly always unintentional. The handler is either incomplete or should return a value.
@Controller('users')
export class UserController {
@Get()
findAll() {} // Empty body
}injectable-must-be-provided
Scope: Project
Detects @Injectable() classes that are not registered in any module's providers array. Infrastructure classes (guards, pipes, filters, interceptors, etc.) are skipped.
A providers array inside a DynamicModule counts. That is the object returned by forRoot(), register(), a static method of any name, a standalone function, or a ConfigurableModuleBuilder setExtras callback. The array can be a local variable, a spread, a push, a helper call, or a ternary. A forRootAsync({ useClass }) option at the call site registers that class too. Test files register nothing.
Why: An @Injectable() class that is not registered anywhere is either dead code or a forgotten registration. NestJS throws when something tries to inject it.
@Injectable()
export class UsersService { // not in any module's providers
findAll() { /* ... */ }
}