Best Practices for Structuring NestJS Projects
Introduction
As your NestJS application grows, having a well-structured codebase becomes crucial for maintainability, scalability, and ease of onboarding new developers. A poorly organized project can lead to technical debt, making it harder to add features, debug issues, or manage dependencies.
In this post, I’ll share best practices for structuring your NestJS project to ensure it remains clean and scalable as your application evolves.
1. Follow a Modular Approach
NestJS is designed with modularity in mind. Breaking your application into feature modules improves separation of concerns and makes the codebase easier to navigate.
Example Structure:
src/
├── app.module.ts
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ └── entities/
│ └── user.entity.ts
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ └── strategies/
│ └── jwt.strategy.ts
└── common/
├── decorators/
├── filters/
├── guards/
├── interceptors/
└── pipes/
Each feature module (e.g., users
, auth
) should handle its own domain logic. Shared resources like guards, interceptors, and custom decorators can live in a common
folder.
2. Use DTOs for Input and Output Validation
Data Transfer Objects (DTOs) ensure consistent data validation and structure across your application. Define DTOs in a dto
folder within each module.
Example: create-user.dto.ts
import { IsString, IsEmail } from "class-validator";
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}
By centralizing DTOs, you avoid duplication and maintain a clear contract for API inputs and outputs.
3. Organize Business Logic with Services
Controllers in NestJS should act as intermediaries, handling HTTP requests and delegating business logic to services. This keeps your controllers lean and focused.
Example:
// users.controller.ts
import { Controller, Get, Post, Body } from "@nestjs/common";
import { UsersService } from "./users.service";
import { CreateUserDto } from "./dto/create-user.dto";
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
findAll() {
return this.usersService.findAll();
}
}
// users.service.ts
import { Injectable } from "@nestjs/common";
@Injectable()
export class UsersService {
private users = [];
create(user: any) {
this.users.push(user);
return user;
}
findAll() {
return this.users;
}
}
4. Leverage NestJS Dependency Injection
NestJS’s built-in dependency injection (DI) system simplifies managing services, repositories, and configurations. Always inject dependencies via the constructor to make testing and refactoring easier.
Example: Injecting ConfigService
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class AuthService {
constructor(private readonly configService: ConfigService) {}
getJwtSecret() {
return this.configService.get<string>("JWT_SECRET");
}
}
5. Group Entities and Repositories
When working with databases, keep your entities and repositories grouped within their respective feature modules. This ensures that each module owns its data models and repository logic.
Example: user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
}
6. Centralize Shared Resources
Put reusable logic like guards, interceptors, and custom pipes in a common
folder. This prevents duplication and makes shared resources easy to find.
Example: Custom Guard
// common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;
return user && user.roles && user.roles.includes("admin");
}
}
7. Configure Environment Variables with ConfigModule
Use the @nestjs/config
package to manage environment variables. This ensures your application’s configuration is centralized and secure.
Install the Package
yarn add @nestjs/config
Example: app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
Accessing Environment Variables
this.configService.get<string>("DATABASE_URL");
8. Use Feature-Specific Testing
Organize your tests alongside their feature modules to make it easier to test individual components.
Example Structure:
users/
├── users.controller.spec.ts
├── users.service.spec.ts
├── users.controller.ts
├── users.service.ts
This ensures that tests are always close to the logic they’re testing, improving maintainability.
9. Use Linting and Formatting Tools
Consistency is key in large projects. Use tools like ESLint and Prettier to enforce coding standards and formatting rules.
Install Linting Tools
yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --dev
yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev
10. Keep Documentation Up-to-Date
A well-documented project is easier to maintain. Use tools like Swagger to document your APIs and README.md files for module-specific information.
Example: Setting Up Swagger
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
const config = new DocumentBuilder()
.setTitle("API Documentation")
.setDescription("The API description")
.setVersion("1.0")
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("api", app, document);
Conclusion
A well-structured NestJS project isn’t just about organization—it’s about creating a foundation for scalability, maintainability, and productivity. By following these best practices, you’ll not only write better code but also save yourself (and your team) countless headaches as your application grows.
How do you structure your NestJS projects? Do you follow similar practices or have unique tips to share? Let’s discuss in the comments!