It can take time to create a new nestjs project and make all settings and install the packages. That's why I made this starter kit.
- Authentication
- Role based authorization
- Refresh token operations
- Session verify and token management from database
- Logout function for killing session
- CurrentUser decorator
- PostgreSQL connection with TypeORM
- Auto load & sync entities
- Eslint
- Swagger (persistAuthorization)
- Env settings (local and dev)
- Debug settings
- Validation setted up and contains sample codes
- Response Format
- Access token, username are indexed for quick access
- Pagination with QueryBuilder
- Static Helper Classes (DtoMapper, StringHelper)
- Slugify integration for URL-friendly strings
- File upload system with validation and thumbnail generation
- Docker configurations for development and production
The project is using Node.js 24.13.0 LTS and NestJS 11.x with Fastify. First of all, if you have nvm, let's make sure you are using version 24.13.0
nvm install 24.13.0
nvm use 24.13.0To install all packages
npm installTo upgrade all packages to current versions
npm update
npm i -g npm-check-updates && ncu -u && npm iTo enable bearer token sending with Swagger, you must put the following tag at the beginning of the controller
@ApiBearerAuth()Using the Roles decorator, you can specify the user roles that can access those controls.
@Roles(Role.Admin, Role.User)
@Get('me')
async getProfile(@CurrentUser() user: Users): Promise<ResponseDto> {
const response = await this.usersService.findOne(user.username);
return new ResponseDto(response);
}All remaining functions are public. You don't need to use @public decorator.
You can return ResponseDto when returning data in the controller to have a uniform response format
const response = await this.authService.getAccessToken(refreshToken);
return new ResponseDto(response, 'Access token retrieved');return new ResponseDto(response); // returns 'ok' message and statusCode:200
return new ResponseDto(response, 'Access token retrieved'); // returns custom message and statusCode:200
return new ResponseDto(null, 'Access token cannot retrieved', HttpStatus.BAD_REQUEST); // returns custom message and statusCode:400
Response Example:
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InN0cmluZyIsInN1YiI6MSwiY3JlYXRlZEF0IjoiMjAyMi0xMS0xMFQxMDo1MDo1MS41MzBaIiwiaWF0IjoxNjY4MTU1MTQ0LCJleHAiOjE2NjgyNDUxNDR9.Xf6AKBTgx6NPXtP7WsqvUJMYdvpUZ_9zZvTTfZpxJyA",
"refresh_token": "c1cb305691112804f045af444fc39a41876bfec25aa544d4cb1ab4e94b05693f743d9c2548afc9c92a8e555777c6bbc50a97fe3bf8fab30eac581e8c42031b0f",
"expires_at": "2022-11-12T09:25:44.918Z",
"expires_refresh_at": "2022-12-11T08:25:44.918Z"
},
"message": "Login informations are retrived",
"statusCode": 200
}Pagination is implemented using TypeORM QueryBuilder with a standardized input DTO. The response follows the PaginatorResponse<T> type structure.
Input DTO (PaginatorInputDto):
export class GetAllUsersDto extends PaginatorInputDto {
// Add custom filters here if needed
}
// Base PaginatorInputDto structure:
{
page: number; // Default: 1
limit: number; // Default: 10, Max: 100
order?: OrderDirection; // 'ASC' | 'DESC', Default: 'DESC'
}Service Implementation:
async getPaginatedUsers(input: GetAllUsersDto): Promise<PaginatorResponse<MeResponseDto>> {
const queryBuilder = this.usersRepository
.createQueryBuilder('users')
.leftJoinAndSelect('users.session_tokens', 'session_tokens')
.orderBy('users.created_at', input.order || 'DESC')
.skip(input.limit * (input.page - 1))
.take(input.limit);
const [data, total] = await queryBuilder.getManyAndCount();
const nodes = DtoMapper.toDtos(data, MeResponseDto);
const pageSize = Math.min(input.limit, total);
return {
nodes,
current_page: input.page,
page_size: pageSize,
has_next: total > input.page * input.limit,
total_pages: Math.ceil(total / input.limit),
total_count: total,
};
}Controller Usage:
@Get('all')
async findAll(@Query() input: GetAllUsersDto): Promise<ResponseDto> {
const result = await this.usersService.getPaginatedUsers(input);
return new ResponseDto(result);
}PaginatorResponse Structure:
{
nodes: T[]; // Array of mapped DTOs
current_page: number; // Current page number
page_size: number; // Number of items in current page
has_next: boolean; // Whether there are more pages
total_pages: number; // Total number of pages
total_count: number; // Total number of records
}The project uses static helper classes instead of injectable services for pure utility functions. This approach provides better performance and cleaner code.
Maps entities to DTOs using class-transformer. No dependency injection needed.
import { DtoMapper } from '../utils';
// Map single entity
const dto = DtoMapper.toDto(user, MeResponseDto);
// Map array of entities
const dtos = DtoMapper.toDtos(users, MeResponseDto);DTO Requirements:
- Use
@Expose()decorator for properties you want to return - Use
@Type(() => NestedDto)for nested objects - Use
@ApiProperty()for Swagger documentation
Example DTO:
export class MeResponseDto {
@Expose()
@ApiProperty()
id: number;
@Expose()
@ApiProperty()
username: string;
@Expose()
@Type(() => SessionResponseDto)
@ApiProperty({ type: [SessionResponseDto] })
session_tokens: SessionResponseDto[];
}Provides string manipulation utilities.
import { StringHelper } from '../utils';
// Generate URL-friendly slug
const slug = StringHelper.generateSlug('Hello World!'); // 'hello-world'
const turkishSlug = StringHelper.generateSlug('Türkçe Başlık'); // 'turkce-baslik'
// Capitalize first letter
const capitalized = StringHelper.capitalize('hello'); // 'Hello'Slugify Configuration:
- Converts to lowercase
- Removes special characters:
*+~.()'"!:@?/ - Handles Turkish characters automatically
- Replaces spaces with hyphens
The project includes a custom throttler guard that works correctly behind proxies (Cloudflare, Nginx, etc.) by detecting the real client IP address.
Usage:
Add @UseGuards(CustomThrottlerGuard) decorator to routes that need rate limiting:
import { CustomThrottlerGuard } from '../common/guards/throttler-behind-proxy.guard';
@Post('login')
@UseGuards(CustomThrottlerGuard)
async login(@Body() loginDto: LoginDto): Promise<ResponseDto> {
const response = await this.authService.login(loginDto);
return new ResponseDto(response);
}IP Detection Priority:
cf-connecting-ip(Cloudflare)true-client-ip(Cloudflare alternative)x-real-ip(Nginx)x-forwarded-for(Standard proxy header)req.ips/req.ip(Direct connection fallback)
The guard automatically detects the real client IP even when behind multiple proxies, preventing rate limit bypass attempts.
The project includes two Docker configurations for different purposes:
Local development infrastructure for testing. Contains Docker Compose configuration to quickly spin up PostgreSQL and Redis locally.
Usage:
cd docker-infra
docker-compose up -d # Start infrastructure
docker-compose down # Stop infrastructureThis folder includes:
docker-compose.yml- PostgreSQL and Redis containersbackup.sh- Database backup scriptrestore.sh- Database restore script
Production deployment example configuration. Contains sample Docker Compose setup that can be used for production deployments.
Contents:
docker-compose.yml- Production-ready compose file with Nginx reverse proxynginx.conf- Nginx configuration for SSL termination and reverse proxybackup.sh- Production database backup scriptrestore.sh- Production database restore scriptssl/- SSL certificate directory
Note: This is a reference configuration. Adjust according to your production environment requirements.
The project includes a comprehensive file upload system with validation, image processing, and thumbnail generation capabilities.
Features:
- Single and multiple file uploads
- File type validation (Image, Video, Document, Audio)
- File size validation
- Image resolution validation (min/max width/height)
- Automatic thumbnail generation for images
- Dimension extraction for images
- Organized file storage with user-based directories
- Database tracking of uploaded files
Single File Upload:
@Post()
@UploadFile('file')
@ApiOperation({ summary: 'Upload a single file' })
async uploadFile(
@UploadedFile() file: MultipartFile,
@CurrentUser() currentUser: Users,
): Promise<ResponseDto<UploadResponseDto>> {
const uploadOptions = new UploadFileDto();
uploadOptions.field_name = 'file';
uploadOptions.file_type = FileTypeEnum.Image;
uploadOptions.max_size = 5120; // 5MB in KB
uploadOptions.max_resolution_width = 2048;
uploadOptions.max_resolution_height = 2048;
uploadOptions.min_resolution_width = 100;
uploadOptions.min_resolution_height = 100;
uploadOptions.return_dimensions = true;
uploadOptions.generate_thumbnails = true;
const result = await this.uploadsService.uploadFile(file, uploadOptions, currentUser);
return new ResponseDto(result, 'File uploaded successfully');
}Multiple File Upload:
@Post('multiple')
@UploadFiles('files', 10)
@ApiOperation({ summary: 'Upload multiple files' })
async uploadMultipleFiles(
@UploadedFiles() files: MultipartFile[],
@CurrentUser() currentUser: Users,
): Promise<ResponseDto<UploadResponseDto[]>> {
const uploadOptions = new UploadFileDto();
uploadOptions.field_name = 'files';
uploadOptions.file_type = FileTypeEnum.Image;
uploadOptions.max_size = 5120; // 5MB
uploadOptions.max_resolution_width = 2048;
uploadOptions.max_resolution_height = 2048;
uploadOptions.min_resolution_width = 100;
uploadOptions.min_resolution_height = 100;
uploadOptions.max_files = 10;
uploadOptions.return_dimensions = true;
uploadOptions.generate_thumbnails = true;
const result = await this.uploadsService.uploadMultipleFiles(files, uploadOptions, currentUser);
return new ResponseDto(result, 'Files uploaded successfully');
}Upload Options (UploadFileDto):
{
field_name: string; // Form field name
file_type: FileTypeEnum; // Image | Video | Document | Audio
max_size: number; // Maximum file size in KB
max_resolution_width?: number; // Max image width (images only)
max_resolution_height?: number; // Max image height (images only)
min_resolution_width?: number; // Min image width (images only)
min_resolution_height?: number; // Min image height (images only)
max_files?: number; // Max number of files (multiple upload)
return_dimensions?: boolean; // Return image dimensions in response
generate_thumbnails?: boolean; // Generate thumbnails for images
}File Type Enum:
enum FileTypeEnum {
Image = 'image', // jpg, jpeg, png, gif, webp, svg
Video = 'video', // mp4, avi, mov, wmv, flv, mkv
Document = 'document', // pdf, doc, docx, xls, xlsx, ppt, pptx, txt
Audio = 'audio', // mp3, wav, ogg, flac, aac
}Upload Response:
{
id: number; // Database record ID
file_name: string; // Original filename
file_path: string; // Relative path to file
file_url: string; // Full URL to access file
file_size: number; // File size in bytes
mime_type: string; // MIME type
width?: number; // Image width (if return_dimensions: true)
height?: number; // Image height (if return_dimensions: true)
thumbnail_path?: string; // Thumbnail path (if generated)
thumbnail_url?: string; // Thumbnail URL (if generated)
uploaded_by: number; // User ID who uploaded
created_at: Date; // Upload timestamp
}File Storage Structure:
public/uploads/
└── user-{userId}/
├── original-filename.jpg
└── thumbnails/
└── original-filename-thumb.jpg
Decorators:
@UploadFile(fieldName)- Single file upload decorator@UploadFiles(fieldName, maxCount)- Multiple files upload decorator@UploadedFile()- Get uploaded file in controller@UploadedFiles()- Get uploaded files array in controller
Validation:
The system automatically validates:
- File type matches allowed extensions
- File size doesn't exceed max_size
- Image dimensions are within min/max bounds
- MIME type matches file extension
Error Handling:
Throws BadRequestException with descriptive messages for:
- Invalid file type
- File size exceeded
- Resolution out of bounds
- Missing required files
- Invalid file format
If you want use test modules checkout nestjs/testing and use spec.ts files. https://docs.nestjs.com/fundamentals/testing