eslint-plugin-nestjs-typed

repository·main·Indexed 42 days ago

https://github.com/darraghoriordan/eslint-plugin-nestjs-typed

An ESLint plugin for NestJS projects designed to prevent common bugs, enforce correct Dependency Injection patterns, ensure strict OpenAPI/Swagger documentation, and improve security. It provides type-aware analysis to detect DI mismatches, enforce Swagger decorator usage, and prevent vulnerabilities related to class-transformer and ValidationPipe configuration. Supports ESLint 9.x (Flat Config) and ESLint 8 (Legacy Config).

Tokens
21.9K
Snippets
62
Records
70
Agent score
49%

What's inside eslint-plugin-nestjs-typed

  1. Overview of NestJS ESLint rules

    main

    This plugin provides ESLint rules specifically designed for NestJS applications to prevent common bugs, security issues, and dependency injection errors. The rules are categorized into four main areas:

    1. Nest Dependency Injection (DI) issues: Detects mismatches in custom providers and ensures injectables are correctly provided to prevent runtime errors.
    2. Open API / Swagger enforcement: Enforces strict typing and decorator usage for NestJS controllers and models to ensure clean Open API specification generation (useful for automatic client generation).
    3. Bug Prevention: Catches issues with untyped decorators and other tightly coupled NestJS features.
    4. Security: Helps prevent vulnerabilities related to class-transformer and ValidationPipe configuration (e.g., addressing CVE-2019-18413).
  2. How sort-module-metadata-arrays works with module arrays

    main

    This rule enforces alphabetical ascending order for arrays within NestJS @Module decorators. It also sorts Call Expressions used within these arrays (e.g., MyModule.forRoot(...)).

    Note on existing projects: The rule processes one entry at a time. If you enable this on an existing project, you may need to run the ESLint --fix command multiple times to fully sort large arrays. Once sorted, it will warn you as you add new items out of order.

    // This will FAIL because AuthzModule is at the end instead of the beginning
    @Module({
        imports: [
            CoreModule,
            DatabaseModule,
            OpenTelemetryModule.forRoot({
                metrics: { hostMetrics: true, apiMetrics: { enable: true } },
            }),
            UserInternalModule,
            AuthzModule,
        ],
        controllers: [AController, BController],
        providers: [AProvider, BProvider],
    })
    export class MainModule {}
    
    // This will PASS because arrays are alphabetical ASC
    @Module({
        imports: [
            AuthzModule,
            CoreModule,
            DatabaseModule,
            OpenTelemetryModule.forRoot({
                metrics: { hostMetrics: true, apiMetrics: { enable: true } },
            }),
            UserInternalModule,
        ],
        controllers: [AController, BController],
        providers: [AProvider, BProvider],
    })
    export class MainModule {}
  3. Optimize Injectables rule scanning with filterFromPaths

    main
    The injectables rule scans your entire project. To improve performance and avoid scanning irrelevant files, use the filterFromPaths configuration setting to exclude specific TypeScript files or directories. Refer to the specific rule documentation for detailed configuration options.
  4. Behavior of sort-module-metadata-arrays with Factory Providers

    main

    The rule specifically ignores the inject array within factory providers. This is because the order of elements in the inject array must strictly match the parameter order of the useFactory function to ensure correct dependency injection. Sorting the inject array would break the application logic.

    @Module({
        providers: [
            {
                provide: 'EXAMPLE_PROVIDER',
                useFactory: (bProvider: BProvider, aProvider: AProvider): unknown => {
                    return new WhatEver();
                },
                // inject array is NOT sorted - it must match the useFactory parameters
                inject: [BProvider, AProvider],
            }
        ]
    })
    export class MainModule {}
  5. Use the param-decorator-name-matches-route-param rule

    main

    The param-decorator-name-matches-route-param rule ensures that the name provided to a @Param("name") decorator matches a corresponding URL parameter defined in either a @Controller() decorator or a method decorator (such as @Get() or @Post()).

    Limitations

    • Regex Params: The rule does not support NestJS fuzzy matching regex parameters (e.g., _+?()*). Routes containing these characters are ignored.
    • String Types: The rule only supports single or double-quoted strings. It does not support variables or template strings for route definitions.

    Correct Usage Patterns

    1. Parameter defined in the method decorator:

    @Controller("custom-bot")
    export class CustomBotController {
        @Get(":uuid")
        findOne(@Param("uuid") uuid: string) {
            // ...
        }
    }

    2. Parameter defined in the controller decorator:

    @Controller("custom-bot/:uuid")
    export class CustomBotController {
        @Get()
        findOne(@Param("uuid") uuid: string) {
            // ...
        }
    }

    Common Errors to Avoid

    1. Typos in the decorator name: If the @Param("name") does not exactly match the string in the route (e.g., @Param("uui") for a :uuid route), the rule will fail.

    2. Including the colon (:) in the @Param decorator: In NestJS, the colon is used in the route string to indicate a parameter, but it should not be included inside the @Param() decorator.

    • ❌ Incorrect: @Param(":uuid")
    • ✅ Correct: @Param("uuid")
    @Controller("custom-bot")
    export class CustomBotController {
        @Get(":uuid")
        findOne(@Param("uuid") uuid: string) {
            return this.customBotService.findOne(uuid);
        }
    }
  6. Use the swagger-file-upload-should-be-documented rule

    main

    The swagger-file-upload-should-be-documented rule ensures that file-upload endpoints are correctly decorated for OpenAPI/Swagger client generation. For a file upload to be understood by generated clients, the endpoint must include both the @ApiConsumes("multipart/form-data") decorator and an @ApiBody() schema decorator.

    If you use a custom composite decorator that provides both of these pieces of metadata, you must configure the rule using the additionalSwaggerDecorators option to prevent false positives.

    // ✅ This passes
    @Post()
    @ApiConsumes("multipart/form-data")
    @ApiBody({ type: FileUploadDto })
    upload(@UploadedFile() file: Express.Multer.File) {}
  7. Configure @darraghor/eslint-plugin-nestjs-typed for ESLint 8 (Legacy Config)

    main

    For ESLint 8 or projects using the legacy configuration format, import classicPlugin from the package and add it to the plugins array in your module.exports.

    import {classicPlugin} from "@darraghor/eslint-plugin-nestjs-typed";
    
    module.exports = {
        plugins: [classicPlugin],
    };
  8. Use `forward-ref-injection-should-use-wrapper-type` to prevent ESM circular dependency issues

    main

    In ESM (ECMAScript Modules) applications, using a class as both the forwardRef() target and the constructor parameter type can cause the class to be evaluated before its module has fully initialized. This rule enforces the use of a wrapper type for the constructor parameter to prevent the direct class reference from being emitted as decorator metadata.

    To fix this, define a generic wrapper type (e.g., WrapperType<T> = T) and use it for the parameter type instead of the class itself. Alternatively, the best long-term solution is to refactor the code to remove the circular dependency.

    This rule is enabled by default in the recommended configuration.

    // Define a wrapper type
    export type WrapperType<T> = T;
    
    // Use the wrapper type in the constructor to pass the lint rule
    constructor(
      @Inject(forwardRef(() => ProfileService))
      private readonly profileService: WrapperType<ProfileService>,
    ) {}
  9. Use the api-method-should-specify-api-response rule

    main

    The api-method-should-specify-api-response rule ensures that NestJS API methods (decorated with methods like @Get()) explicitly specify their return status codes and types using @ApiResponse or other response decorators. This helps in maintaining accurate OpenAPI/Swagger documentation.

    While 400 and 500 level errors are often assumed, you should use these decorators if the return type changes for those error states.

    class TestClass {
        @Get()
        @ApiResponse({status: 200, type: String})
        @ApiBadRequestResponse({description: "Bad Request"})
        public getAll(): Promise<string[]> {
            return [];
        }
    }
  10. Use the all-properties-have-explicit-defined rule

    main

    The all-properties-have-explicit-defined rule ensures that every property in a class intended for validation has an explicit validation presence decorator: @IsDefined(), @IsOptional(), or @ValidateIf().

    Key behaviors:

    • Conflict Prevention: It prevents invalid combinations such as @IsDefined() with @IsOptional(), or @IsOptional() with @ValidateIf().
    • Valid Patterns: Combining @ValidateIf() with @IsDefined() is permitted for conditional validation on optional properties.
    • Automatic Exclusion: To prevent false positives on non-validation classes, the rule ignores any class that contains zero class-validator decorators.
    // PASSES - all properties are decorated correctly
    export class CreateOrganisationDto {
        @IsDefined()
        otherProperty!: MyClass;
    
        @IsOptional()
        someStringProperty?: string;
    
        @ValidateIf(o => !o.someStringProperty)
        @Length(10, 20)
        someOtherProperty?:  string
    }
    
    // FAILS - missing @IsOptional() or @ValidateIf() on someStringProperty
    export class CreateOrganisationDto {
        @IsDefined()
        otherProperty!: MyClass;
        @IsString()
        someStringProperty?: string;
    }
  11. Prevent duplicate decorators with no-duplicate-decorators

    main

    Use the no-duplicate-decorators rule to ensure that decorators which should only be applied once are not accidentally duplicated on classes or properties. This prevents bugs where the NestJS runtime or other metadata-driven logic might ignore subsequent instances of a decorator.

    // PASSES - all properties or classes are decorated once
    @DiscoverDecorator()
    class MyClass {
        @DiscoverDecorator()
        myProperty: string;
    }
    
    // FAILS - one property here has an invalid decorator applied twice
    @DiscoverDecorator()
    class MyClass {
        @DiscoverDecorator()
        @DiscoverDecorator()
        myProperty: string;
    }