A production-ready NestJS starter template with Docker, PM2, and best practices pre-configured. This template provides a solid foundation for building scalable Node.js applications with TypeScript. It includes powerful abstract CRUD services and entities that accelerate development by providing consistent patterns for data operations.
- π NestJS v11 - Progressive Node.js framework
- π TypeScript - Type safety and modern JavaScript features
- π³ Docker Ready - Containerized development and deployment
- βοΈ PM2 Configuration - Production process management
- π Swagger/OpenAPI - Auto-generated API documentation
- β Testing Setup - Unit and E2E tests with Jest
- π― ESLint & Prettier - Code quality and formatting
- π§ Environment Configuration - Secure configuration management
- ποΈ Abstract CRUD System - Reusable base classes for entities, services, and controllers
- π Advanced Querying - Built-in pagination, search, sorting, and filtering
- πΎ MongoDB Integration - Mongoose with transaction support and retry logic
- Node.js 22+ and npm
- Docker and Docker Compose (optional)
- Git
To quickly rename this project from nest-starter to your own project name, use these sed commands:
# Replace in all relevant files
find . -type f \( -name "*.json" -o -name "*.yml" -o -name "*.js" -o -name "Caddyfile" \) \
-not -path "./node_modules/*" \
-not -path "./.git/*" \
-exec sed -i '' 's/nest-starter/your-project-name/g' {} +# Replace in all relevant files
find . -type f \( -name "*.json" -o -name "*.yml" -o -name "*.js" -o -name "Caddyfile" \) \
-not -path "./node_modules/*" \
-not -path "./.git/*" \
-exec sed -i 's/nest-starter/your-project-name/g' {} +package.json- Project namepackage-lock.json- Package lock referencesdocker-compose.yml- Container and image namesecosystem.config.js- PM2 app nameCaddyfile- Domain configuration
Note: After renaming, run npm install to update package-lock.json properly.
# Clone the repository
git clone https://github.com/yourusername/nest-starter.git
cd nest-starter
# Install dependencies
npm installmkdir mongodb
openssl rand -base64 756 > ./mongodb/mongo-local-dev-keyfile
chmod 600 ./mongodb/mongo-local-dev-keyfileecho "requirepass devredispass\nappendonly yes" > ./redis.dev.conf# Build and run with Docker Compose
docker-compose up -d
# View logs
docker-compose logs -f appsh ./scripts/init-replica.sh --env devCreate a .env file in the root directory:
# Application
NODE_ENV=development
PORT=3000
# Database
DATABASE_URL=mongodb://localhost:27017/nest-starter
# API Documentation
SWAGGER_USER=admin
SWAGGER_PASSWORD=changeme# Development with hot reload
npm run start:dev
# Development with debug
npm run start:debug
# Production build
npm run build
npm run start:prod
# Format code
npm run format
# Lint code
npm run lintSwagger documentation is available at:
- Local: http://localhost:3000/api
- Production: https://your-domain.com/api
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov# Start application with PM2
pm2 start ecosystem.config.js
# Monitor
pm2 monit
# Reload with zero downtime
pm2 reload all# Build production image
docker build -t nest-starter:latest .
# Run container
docker run -d \
--name nest-app \
-p 3000:3000 \
--env-file .env \
nest-starter:latest# Deploy with Docker Compose
docker-compose -f docker-compose.yml up -d
# Scale horizontally
docker-compose up -d --scale app=3nest-starter/
βββ src/
β βββ main.ts # Application entry point
β βββ app.module.ts # Root module
β βββ app.controller.ts # Root controller
β βββ app.service.ts # Root service
βββ test/
β βββ app.e2e-spec.ts # E2E tests
β βββ jest-e2e.json # Jest E2E configuration
βββ docker-compose.yml # Docker Compose configuration
βββ Dockerfile # Docker image definition
βββ ecosystem.config.js # PM2 configuration
βββ Caddyfile # Caddy server configuration
βββ package.json # Project metadata and scripts
| Script | Description |
|---|---|
npm run start |
Start application |
npm run start:dev |
Start in watch mode |
npm run start:debug |
Start with debugger |
npm run start:prod |
Start production build |
npm run build |
Build application |
npm run format |
Format code with Prettier |
npm run lint |
Lint code with ESLint |
npm run test |
Run unit tests |
npm run test:watch |
Run tests in watch mode |
npm run test:cov |
Generate test coverage |
npm run test:e2e |
Run E2E tests |
This template includes a powerful abstract CRUD system that provides reusable base classes for entities, services, and controllers. This system accelerates development by providing consistent patterns for data operations with built-in features like pagination, search, sorting, and transaction support.
The base abstract entity provides common fields for all entities:
import { Schema } from '@nestjs/mongoose';
import { AbstractEntity } from './src/common/entities/abstract.entity';
@Schema()
export class Product extends AbstractEntity {
// already auto inherited name, description and metadata
// Your custom fields here
@Prop()
sku: string;
}Fields included:
user: Reference to the user who created the entity (optional)name: Entity name (optional)description: Entity description (optional)metadata: Flexible key-value storage (optional)
The abstract service provides complete CRUD operations with MongoDB transaction support ensuring that an operation wholly succeeds or fails:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { AbstractService } from './src/common/services/abstract.service';
import { TransactionManager } from './src/common/services/transaction.manager';
import { Product, ProductDocument } from './entities/product.entity';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
@Injectable()
export class ProductService extends AbstractService<
ProductDocument,
CreateProductDto,
UpdateProductDto
> {
constructor(
@InjectModel(Product.name) model: Model<ProductDocument>,
transactionManager: TransactionManager,
) {
super(model, transactionManager);
}
// Override populator if needed
get populator(): string[] {
return ['user', 'customRelation'];
}
}The abstract controller provides standard REST endpoints:
import { Controller } from '@nestjs/common';
import { AbstractController } from './src/common/controllers/abstract.controller';
import { ProductDocument } from './entities/product.entity';
import { ProductService } from './services/product.service';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
@Controller('products')
export class ProductController extends AbstractController<
ProductDocument,
CreateProductDto,
UpdateProductDto
> {
constructor(private readonly productService: ProductService) {
super(productService);
}
}This automatically makes the following endpoints available:
POST /products- Create a new productGET /products- List products with paginationGET /products/:id- Get a product by IDPATCH /products/:id- Update a product
| Method | Description | Usage |
|---|---|---|
create(dto, user?, session?) |
Creates a new entity | Basic entity creation |
findMany(query, session?) |
Retrieves entities with pagination | List entities with filters |
findOne(id, session?) |
Retrieves a single entity | Get entity by ID |
remove(id, session?) |
Deletes an entity | Delete entity |
- Purpose: Standard partial update of entity fields
- When to use: When you want to update only specific fields that are included in your UpdateDto
- Behavior: Only updates fields present in the DTO, ignores undefined/null values
- Example:
// Only updates description and metadata fields
await service.update(id, {
description: 'New description',
metadata: { updated: true }
});- Purpose: Direct database update with any fields
- When to use: When you need to update fields not included in your UpdateDto or perform complex updates
- Behavior: Directly passes the update object to MongoDB's
findByIdAndUpdate - Example:
// Can update any field, even those not in UpdateDto
await service.forceUpdate(id, {
customField: 'new value',
'metadata.nested.field': 'deep update',
lastModified: new Date()
});- Purpose: Atomically increment/decrement numeric fields
- When to use: For counters, scores, quantities, or any numeric operations that need to be atomic
- Behavior: Uses MongoDB's
$incoperator for atomic operations - Example:
// Increment view count and decrease stock
await service.increment(id, {
viewCount: 1,
stockQuantity: -1,
'metrics.totalClicks': 5
});The findMany method supports comprehensive querying options:
const query = {
// Text search across indexed fields
search: 'search term',
// Pagination
page: 1,
limit: 10,
// Sorting (field:order format)
sort: 'createdAt:desc,name:asc',
// Field filtering (supports MongoDB ObjectIds)
user: '507f1f77bcf86cd799439011',
status: 'active',
// Metadata filtering
metadata: { category: 'tech' }
};
const result = await service.findMany(query);{
data: MyEntity[],
pagination: {
total: 100,
page: 1,
limit: 10,
totalPages: 10
}
}All service methods support MongoDB transactions via the optional session parameter:
const session = await this.connection.startSession();
session.startTransaction();
try {
const entity1 = await service1.create(dto1, user, session);
const entity2 = await service2.update(id, dto2, session);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}The abstract service includes automatic retry logic and session management through the withSession method.
AbstractCreateDto:
{
name: string; // Required
description?: string; // Optional
user?: Types.ObjectId; // Optional (auto-set from auth)
metadata?: Record<string, unknown>; // Optional
}AbstractUpdateDto:
{
description?: string; // Optional
metadata?: Record<string, unknown>; // Optional
}QueryDto:
{
search?: string; // Text search
page?: number; // Page number (min: 1)
limit?: number; // Items per page (1-100)
sort?: string; // Sort format: "field:order"
[key: string]: unknown; // Additional filters
metadata?: Record<string, unknown>; // Metadata filters
}- Always extend base classes - Don't recreate CRUD functionality
- Use transactions for related operations - Pass session between service calls
- Override populators - Specify relationships to populate in service
- Leverage slug entities - For public-facing resources needing SEO URLs
- Use appropriate update methods:
update()for standard user input validationforceUpdate()for system-level updatesincrement()for atomic numeric operations
- Implement proper validation - Extend base DTOs with your validation rules
- Configure text search - Add text indexes to your schemas for search functionality
This template includes a comprehensive Logto Management API integration for authentication and authorization.
Add the following environment variables:
LOGTO_ENDPOINT=https://your-tenant.logto.app
LOGTO_TENANT_ID=your-tenant-id
LOGTO_APP_ID=your-app-id
LOGTO_APP_SECRET=your-app-secret
LOGTO_API_RESOURCE_ID=your-api-resource-id
LOGTO_ORGANIZATION_ID=your-organization-id
LOGTO_WEBHOOK_SECRET=your-webhook-signing-keyThe Logto module provides grouped services by functionality:
| Service | Description |
|---|---|
LogtoUsersService |
User management (CRUD, suspension, roles, scopes) |
RolesService |
Role management with scope assignment |
PermissionsService |
Permission/scope management |
OrganizationsService |
Organization management, user membership, invitations |
import { LogtoModule, LogtoAuthGuard, Public, Scopes, GetCurrentUser } from './logto';
// Import the module
@Module({
imports: [LogtoModule],
})
export class AppModule {}
// Apply guard globally or per-controller
@UseGuards(LogtoAuthGuard)
@Controller('protected')
export class ProtectedController {
@Public() // Mark route as public
@Get('public')
publicRoute() {}
@Scopes('read:users') // Require specific permissions
@Get('admin')
adminRoute(@GetCurrentUser() user: CurrentUser) {
return user;
}
}| Decorator | Description |
|---|---|
@Public() |
Marks a route as publicly accessible |
@Scopes(...scopes) |
Requires specific permission scopes |
@GetCurrentUser() |
Injects the authenticated user into route handler |
The template includes a webhook endpoint for Logto events with signature verification.
Endpoint: POST /users/webhook/logto
Supported Events:
| Event | Action |
|---|---|
User.Created |
Onboard user (via Management API) |
PostRegister |
Onboard user (via sign-up UI) |
Setup:
- In Logto Console, go to Webhooks and create a new webhook
- Set the endpoint URL to
https://your-domain.com/users/webhook/logto - Select the events you want to receive
- Copy the signing key and set it as
LOGTO_WEBHOOK_SECRETin your environment
Signature Verification:
The webhook endpoint verifies the logto-signature-sha-256 header using HMAC-SHA256. If LOGTO_WEBHOOK_SECRET is not configured, signature verification is skipped (with a warning).
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
This project is MIT licensed.
- Built with NestJS
- Inspired by best practices from the NestJS community