This guide covers the development workflow, best practices, and tooling for the NestJS API Starter Kit. Follow these guidelines to maintain code quality and consistency across the project.
- Development Workflow
- Code Organization
- Coding Standards
- Git Workflow
- IDE Setup
- Debugging
- Hot Reloading
- Database Development
- API Development
- Error Handling
- Logging
- Performance Optimization
- Security Considerations
-
Start Development Environment
# Pull latest changes git pull origin main # Install any new dependencies npm install # Start development server npm run start:dev # Or with Docker docker compose up -d
-
Feature Development Process
# Create feature branch git checkout -b feature/user-authentication # Make your changes # ... development work ... # Run tests npm run test:all # Check code quality npm run lint npm run format:check # Commit changes git add . git commit -m "feat: implement user authentication" # Push and create PR git push origin feature/user-authentication
-
Pre-commit Checklist
- All tests pass (
npm run test:all) - Code follows style guide (
npm run lint) - Code is formatted (
npm run format) - No TypeScript errors (
npm run build) - Database migrations run successfully
- Documentation updated if needed
- All tests pass (
# Development mode (default)
NODE_ENV=development npm run start:dev
# Debug mode with inspector
npm run start:debug
# Production simulation
NODE_ENV=production npm run build && npm run start:prod
# Test environment
NODE_ENV=test npm run test:e2eOrganize code by domain modules rather than technical layers:
src/
├── app.module.ts # Root module
├── main.ts # Bootstrap file
├── common/ # Shared utilities
│ ├── decorators/ # Custom decorators
│ ├── filters/ # Exception filters
│ ├── guards/ # Route guards
│ ├── interceptors/ # HTTP interceptors
│ └── pipes/ # Validation pipes
├── config/ # Configuration
├── database/ # Database setup
├── health/ # Health checks
└── users/ # User domain module
├── users.module.ts
├── users.controller.ts
├── users.service.ts
├── users.entity.ts
├── dto/
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
└── tests/
├── users.controller.spec.ts
└── users.service.spec.ts
- Controllers:
*.controller.ts - Services:
*.service.ts - Entities:
*.entity.ts - DTOs:
*.dto.ts - Interfaces:
*.interface.ts - Types:
*.type.ts - Modules:
*.module.ts - Tests:
*.spec.tsor*.test.ts - E2E Tests:
*.e2e-spec.ts
Use the NestJS CLI to maintain consistency:
# Generate a complete module with controller and service
nest g resource users
# Generate individual components
nest g module users
nest g controller users
nest g service users
nest g interface usersOrganize imports in the following order:
// 1. Node.js built-in modules
import { readFileSync } from 'fs';
// 2. External libraries
import { Injectable, Logger } from '@nestjs/common';
import { Repository } from 'typeorm';
// 3. Internal modules (absolute paths)
import { User } from '@/users/user.entity';
import { DatabaseService } from '@/database/database.service';
// 4. Relative imports
import { CreateUserDto } from './dto/create-user.dto';
import { UserInterface } from './interfaces/user.interface';-
Strict Type Checking
// Good: Explicit types function createUser(userData: CreateUserDto): Promise<User> { return this.userRepository.save(userData); } // Avoid: Any types function createUser(userData: any): Promise<any> { return this.userRepository.save(userData); }
-
Interface over Type for Objects
// Good: Interface for object shapes interface UserConfig { maxRetries: number; timeout: number; } // Good: Type for unions and computed types type UserStatus = 'active' | 'inactive' | 'banned'; type UserKeys = keyof User;
-
Proper Error Handling
@Injectable() export class UsersService { async findUser(id: string): Promise<User> { try { const user = await this.userRepository.findOne({ where: { id } }); if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user; } catch (error) { this.logger.error(`Failed to find user ${id}`, error.stack); throw error; } } }
-
Dependency Injection
@Injectable() export class UsersService { constructor( @InjectRepository(User) private readonly userRepository: Repository<User>, private readonly logger: Logger, ) {} }
-
DTOs for Validation
import { IsEmail, IsString, MinLength } from 'class-validator'; export class CreateUserDto { @IsString() @MinLength(2) name: string; @IsEmail() email: string; @IsString() @MinLength(8) password: string; }
-
Proper Module Structure
@Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService], exports: [UsersService], // Export if used by other modules }) export class UsersModule {}
The project uses ESLint and Prettier with the following key rules:
- Indentation: 2 spaces
- Quotes: Single quotes for strings
- Semicolons: Required
- Trailing commas: Always
- Line length: 100 characters
- Object literals: Multi-line when exceeding line length
// Good example following style guide
const userConfig = {
name: 'John Doe',
email: 'john@example.com',
settings: {
notifications: true,
theme: 'dark',
},
};Use Git Flow with the following branch types:
main- Production-ready codedevelop- Integration branch for featuresfeature/*- New featuresbugfix/*- Bug fixeshotfix/*- Critical production fixesrelease/*- Release preparation
Follow Conventional Commits:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Types:
feat- New featurefix- Bug fixdocs- Documentation changesstyle- Code style changes (formatting, etc.)refactor- Code refactoringtest- Adding or updating testschore- Maintenance tasks
Examples:
feat(auth): implement JWT authentication
fix(users): resolve email validation issue
docs(api): update endpoint documentation
test(users): add unit tests for user service
-
Create Feature Branch
git checkout -b feature/user-profile
-
Make Changes and Commit
git add . git commit -m "feat(users): add user profile endpoint"
-
Push and Create PR
git push origin feature/user-profile
-
PR Requirements
- All checks pass (CI/CD)
- Code review approved
- Tests cover new functionality
- Documentation updated
- No merge conflicts
Recommended Extensions:
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-next",
"ms-vscode.vscode-json",
"bradlc.vscode-tailwindcss",
"ms-vscode-remote.remote-containers",
"ms-azuretools.vscode-docker",
"ckolkman.vscode-postgres"
]
}Settings (.vscode/settings.json):
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"typescript.preferences.importModuleSpecifier": "relative",
"files.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/.git": true
}
}Debug Configuration (.vscode/launch.json):
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch NestJS",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/src/main.ts",
"args": [],
"runtimeArgs": [
"-r",
"ts-node/register",
"-r",
"tsconfig-paths/register"
],
"sourceMaps": true,
"envFile": "${workspaceFolder}/.env",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
}
]
}- Enable ESLint: File → Settings → Languages & Frameworks → JavaScript → Code Quality Tools → ESLint
- Enable Prettier: File → Settings → Languages & Frameworks → JavaScript → Prettier
- Configure TypeScript: File → Settings → Languages & Frameworks → TypeScript
- Set up debugging: Run → Edit Configurations → Add Node.js configuration
-
Start Debug Server
npm run start:debug
-
Attach Debugger
- VS Code: Use F5 or debug configuration
- Chrome DevTools: Navigate to
chrome://inspect - WebStorm: Attach to Node.js process
-
Set Breakpoints
@Get(':id') async findOne(@Param('id') id: string) { debugger; // Breakpoint here return this.usersService.findOne(id); }
-
Modify Docker Compose for Debugging
api: command: npm run start:debug ports: - '3000:3000' - '9229:9229' # Debug port
-
Attach Remote Debugger
{ "name": "Docker: Attach to Node", "type": "node", "request": "attach", "port": 9229, "address": "localhost", "localRoot": "${workspaceFolder}", "remoteRoot": "/usr/src/app", "protocol": "inspector" }
- Use proper logging levels for different environments
- Log request IDs for tracing requests across services
- Use structured logging with context objects
- Enable source maps for better stack traces
Hot reloading is enabled by default in development:
npm run start:devFeatures:
- Automatic restart on file changes
- Preserves application state where possible
- Fast compilation with webpack HMR
- TypeScript incremental compilation
The project uses webpack Hot Module Replacement:
// webpack-hmr.config.js
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}-
Files not being watched
# Increase inotify limit (Linux) echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf sudo sysctl -p
-
Slow reload times
- Exclude
node_modulesfrom file watching - Use TypeScript project references
- Optimize webpack configuration
- Exclude
-
Make Entity Changes
@Entity() export class User { @Column() firstName: string; // New field added }
-
Generate Migration
npm run typeorm:migration:generate -- -n AddFirstNameToUser
-
Review Generated Migration
export class AddFirstNameToUser implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.addColumn( 'user', new TableColumn({ name: 'firstName', type: 'varchar', }), ); } }
-
Run Migration
npm run typeorm:migration:run
- Always use migrations for schema changes
- Never edit existing migrations after they're merged
- Include rollback logic in down methods
- Test migrations on copies of production data
- Use indexes for frequently queried columns
// src/database/seeds/user.seed.ts
export class UserSeed implements Seeder {
async run(dataSource: DataSource): Promise<void> {
const userRepository = dataSource.getRepository(User);
const users = [
{ name: 'Admin User', email: 'admin@example.com' },
{ name: 'Test User', email: 'test@example.com' },
];
await userRepository.save(users);
}
}Follow REST principles:
@Controller('users')
export class UsersController {
@Get() // GET /users
findAll() {}
@Get(':id') // GET /users/:id
findOne(@Param('id') id: string) {}
@Post() // POST /users
create(@Body() createUserDto: CreateUserDto) {}
@Put(':id') // PUT /users/:id
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {}
@Delete(':id') // DELETE /users/:id
remove(@Param('id') id: string) {}
}@Controller({
path: 'users',
version: '1',
})
export class UsersV1Controller {}
@Controller({
path: 'users',
version: '2',
})
export class UsersV2Controller {}import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
@IsOptional()
@IsString()
phone?: string;
}@UseInterceptors(ClassSerializerInterceptor)
export class User {
@Exclude()
password: string;
@Expose()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = this.getStatus(exception);
const message = this.getMessage(exception);
const errorResponse = {
success: false,
error: {
code: this.getErrorCode(exception),
message,
timestamp: new Date().toISOString(),
path: request.url,
},
};
response.status(status).json(errorResponse);
}
}export class UserNotFoundException extends NotFoundException {
constructor(userId: string) {
super(`User with ID ${userId} not found`, 'USER_NOT_FOUND');
}
}@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
async createUser(userData: CreateUserDto): Promise<User> {
this.logger.log({
action: 'create_user',
email: userData.email,
timestamp: new Date().toISOString(),
});
try {
const user = await this.userRepository.save(userData);
this.logger.log({
action: 'user_created',
userId: user.id,
email: user.email,
});
return user;
} catch (error) {
this.logger.error({
action: 'create_user_failed',
email: userData.email,
error: error.message,
});
throw error;
}
}
}error- Error conditionswarn- Warning conditionsinfo- Informational messagesdebug- Debug-level messagesverbose- Verbose logging
// Use select to limit returned fields
const users = await this.userRepository.find({
select: ['id', 'name', 'email'],
where: { active: true },
});
// Use relations efficiently
const userWithPosts = await this.userRepository.findOne({
where: { id },
relations: ['posts'],
});
// Use query builder for complex queries
const users = await this.userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('user.active = :active', { active: true })
.getMany();@Injectable()
export class UsersService {
@Cacheable(600) // Cache for 10 minutes
async findAllUsers(): Promise<User[]> {
return this.userRepository.find();
}
}Already configured in main.ts:
import compression from 'compression';
app.use(compression());- Use DTOs for all input validation
- Sanitize input to prevent injection attacks
- Validate file uploads carefully
- Implement rate limiting on sensitive endpoints
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get('admin-only')
adminOnlyEndpoint() {
return 'Admin content';
}Configured via Helmet:
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
},
},
}),
);- Never commit secrets to version control
- Use strong secrets in production
- Rotate secrets regularly
- Use different secrets for each environment
This development guide provides the foundation for maintaining high-quality, consistent code in the NestJS API Starter Kit. Follow these practices to ensure your application remains scalable, maintainable, and secure as it grows.
Next: Learn about Testing Strategies to ensure code quality and reliability.