| name | nestjs |
| description | [Applies to: **/*.{ts,tsx}] This guide provides opinionated, actionable best practices for building robust, scalable, and maintainable NestJS applications using TypeScript, emphasizing modern patterns and common pitfalls. |
| source | cursor_mdc |
NestJS Best Practices
NestJS is a powerful framework for building scalable Node.js applications. Adhering to these guidelines ensures your projects are maintainable, performant, and secure.
1. Code Organization and Structure
Adopt a feature-based, semi-hexagonal architecture. Each business domain gets its own module.
✅ GOOD: Feature-based Modules
Organize code by feature, not by type.
@Module({
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService],
})
export class UsersModule {}
❌ BAD: Type-based Flat Structure
Avoid scattering related logic across different top-level folders.
// src/controllers/users.controller.ts
// src/services/users.service.ts
Naming Conventions
Follow standard TypeScript conventions for clarity:
- Files/Folders:
kebab-case (e.g., user-profile.module.ts)
- Classes:
PascalCase (e.g., UsersService, CreateUserDto)
- Interfaces:
IPascalCase (e.g., IUserRepository)
- Functions/Methods:
camelCase (e.g., createUser, findAllUsers)
- Constants:
UPPER_SNAKE_CASE (e.g., JWT_SECRET)
2. Type Safety and Strictness
Always use explicit types. Avoid any. Enable strict TypeScript in tsconfig.json.
✅ GOOD: Explicit Types
Every variable, parameter, and return value must have an explicit type.
interface IUser { id: string; name: string; email: string; }
async function fetchUserById(id: string): Promise<IUser | null> {
const user: IUser | undefined = await this.userRepository.findOne(id);
return user ?? null;
}
❌ BAD: Implicit any
Leads to runtime errors and defeats TypeScript's purpose.
function fetchUserById(id) {
const user = this.userRepository.findOne(id);
return user;
}
3. Common Patterns
Dependency Injection (DI)
Leverage NestJS's DI system for loose coupling and testability.
✅ GOOD: Constructor Injection
Always inject dependencies via the constructor.
@Injectable()
export class UsersService {
constructor(private readonly userRepository: IUserRepository) {}
async findAll(): Promise<IUser[]> { return this.userRepository.findAll(); }
}
❌ BAD: Manual Instantiation
Avoid new keyword for services/providers.
const userService = new UsersService();
Data Transfer Objects (DTOs)
Use DTOs with class-validator for all incoming request bodies and query parameters.
✅ GOOD: Validated DTOs
Ensure data integrity at the edge of your application.
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString() @MinLength(3) name: string;
@IsEmail() email: string;
@IsString() @MinLength(8) password: string;
}
@Post()
async createUser(@Body() createUserDto: CreateUserDto): Promise<IUser> {
return this.userService.create(createUserDto);
}
❌ BAD: Direct Request Body Usage
Unvalidated data is a security risk.
@Post()
async createUser(@Body() body: any): Promise<IUser> { }
Controllers and Services
Controllers handle HTTP requests and delegate business logic to services. Services contain the core business logic and interact with data layers.
✅ GOOD: Lean Controllers, Rich Services
Controllers should be thin, focusing on request/response.
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
async findOne(@Param('id') id: string): Promise<IUser> {
return this.usersService.findById(id);
}
}
@Injectable()
export class UsersService {
constructor(private readonly userRepository: IUserRepository) {}
async findById(id: string): Promise<IUser> {
const user = await this.userRepository.findOne(id);
if (!user) { throw new NotFoundException(); }
user;
}
}
❌ BAD: Business Logic in Controllers
Makes controllers hard to test and re-use.
@Controller('users')
export class UsersController {
constructor(private readonly userRepository: IUserRepository) {}
@Get(':id')
async findOne(@Param('id') id: string): Promise<IUser> {
const user = await this.userRepository.findOne(id);
if (!user) { throw new NotFoundException(`User ID "${id}" not found.`); }
return user;
}
}
4. Error Handling
Centralize error handling using Exception Filters.
✅ GOOD: Global Exception Filters
Catch and format all unhandled exceptions consistently.
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>(); const status = exception.getStatus();
response.status(status).json({
statusCode: status, timestamp: new Date().toISOString(), path: request.url,
message: exception.message,
});
}
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.();
}
❌ BAD: try/catch in Every Method
Leads to boilerplate and inconsistent error responses.
@Get(':id')
async findOne(@Param('id') id: string): Promise<IUser> {
try { return await this.usersService.findById(id); }
catch (error) { throw new HttpException('Failed', HttpStatus.INTERNAL_SERVER_ERROR); }
}
5. Security Best Practices
Guards for Authorization
Use Guards for declarative route protection.
✅ GOOD: Role-Based Access Control (RBAC)
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
context.getHandler(), context.getClass(),
]);
if (!requiredRoles) { return true; }
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
@Get() @Roles(UserRole.)
(): <[]> { ..(); }
}
Environment Variables
Manage configuration securely using @nestjs/config.
✅ GOOD: ConfigModule
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true, envFilePath: `.env.${process.env.NODE_ENV}` })],
})
export class AppModule {}
@Injectable()
export class DatabaseService {
constructor(private configService: ConfigService) {
const dbHost = this.configService.get<string>('DATABASE_HOST');
}
}
❌ BAD: Hardcoding Secrets
Never hardcode sensitive information.
const JWT_SECRET = 'super-secret-key';
6. Performance Considerations
Fastify Adapter
Use Fastify for improved performance over Express.
✅ GOOD: Fastify Integration
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule, new FastifyAdapter()
);
await app.listen(3000);
}
bootstrap();
Caching
Implement caching for frequently accessed, slow-changing data.
✅ GOOD: CacheModule
@Module({
imports: [CacheModule.register({ store: redisStore, host: 'localhost', port: 6379, ttl: 300 })],
})
export class AppModule {}
@Get() @UseInterceptors(CacheInterceptor) @CacheKey('all_users') @CacheTTL(60)
async findAll(): Promise<IUser[]> { return this.usersService.findAll(); }
7. Testing
Prioritize unit tests for services and e2e tests for controllers.
✅ GOOD: Unit Testing Services
Mock external