A production-ready, enterprise-grade starter template for building scalable backend applications with NestJS, Prisma ORM, and PostgreSQL.
- Why This Starter?
- Features
- Tech Stack
- Quick Start
- System Overview
- Project Structure
- Getting Started
- API Documentation
- Environment Configuration
- API Reference
- Adding Protected Routes
- Extending the Template
- Frontend Integration
- Error Handling
- Docker Setup
- Monitoring Stack
- CI/CD Pipeline
- Testing
- Production Deployment
- Security Best Practices
- Troubleshooting
- Contributing
- License
This isn't just another boilerplateβit's a battle-tested, production-grade foundation that implements enterprise security patterns and best practices out of the box:
| Feature | Why It Matters |
|---|---|
| JWT + Refresh Token Rotation | Prevents token theft with automatic rotation on each refresh |
| Rate Limiting & Account Lockout | Protects against brute force attacks |
| Timing Attack Prevention | Consistent response times prevent user enumeration |
| Distributed Locks (Redis) | Prevents race conditions in concurrent operations |
| Token Version for Instant Revocation | Immediately invalidate all user sessions on security events |
| Centralized Logging (Loki) | Aggregate logs from all services for debugging |
| Prometheus Metrics | Real-time performance monitoring and alerting |
| Multi-stage Docker Builds | Optimized production images (~50% smaller) |
Get up and running in 5 minutes:
# 1. Clone the repository
git clone https://github.com/the-pujon/nestjs-prisma-postgres-starter.git
cd nestjs-prisma-postgres-starter
# 2. Create environment file
cp .env.example .env
# 3. Start Docker services (PostgreSQL, Redis, Prometheus, Grafana, Loki)
docker compose up -d
# 4. Install dependencies
npm install
# 5. Run database migrations
npx prisma migrate dev
# 6. Start the development server
npm run start:devVerify installation:
- API Health: http://localhost:5000
- API Docs: Import
postman-collection.jsoninto Postman - PgAdmin: http://localhost:8080 (admin@example.com / admin)
- RedisInsight: http://localhost:8001
- Grafana: http://localhost:3000 (admin / admin)
- Prometheus: http://localhost:9090
- JWT Authentication with access & refresh tokens
- Token Rotation - New refresh token on each refresh (prevents token theft)
- Email Verification with 6-digit OTP codes (24h expiry)
- Google OAuth 2.0 - Complete social login integration
- Rate Limiting (per-email, per-IP based)
- Account Lockout - 30 min lockout after 5 failed attempts
- Password Strength Validation - Min 8 chars with complexity requirements
- Hybrid Token Validation (Redis cache + DB fallback for speed)
- Timing Attack Prevention - Consistent response times
- Async Email Processing with BullMQ job queue
- Automatic Retries - 3 attempts with exponential backoff
- Email Templates - HTML templates for verification & welcome emails
- Email History Tracking - Full audit trail in database
- Multiple Email Types - Verification, password reset, notifications
- Google OAuth 2.0 - Login with Google account
- Provider Abstraction - Easy to add more providers (GitHub, Facebook)
- Account Linking - Link OAuth to existing accounts
- Secure Callback Handling - State validation and token exchange
- PostgreSQL 17 with Prisma ORM (modular schema)
- Redis Stack for caching, sessions, and distributed locks
- BullMQ for background job processing (emails, notifications)
- Docker Compose for local development
- Multi-stage Docker builds for production (~50% smaller images)
- Prometheus metrics collection (request duration, errors, active users)
- Grafana dashboards (auto-provisioned)
- Loki log aggregation (structured JSON logs)
- Winston structured logging with multiple transports
- Health checks on startup
- TypeScript 5.7 with strict mode
- ESLint + Prettier configured
- Unit tests with Jest + mocks
- Postman collection included (all endpoints)
- Hot reload in development
- CI/CD with GitHub Actions (Docker Hub + EC2)
| Category | Technology |
|---|---|
| Framework | NestJS 11 |
| Language | TypeScript 5.7 |
| ORM | Prisma 7 |
| Database | PostgreSQL 17 |
| Cache/Queue | Redis Stack (with RedisInsight UI) |
| Job Queue | BullMQ |
| Auth | JWT (jsonwebtoken) |
| Validation | class-validator, class-transformer |
| Logging | Winston + Loki |
| Metrics | Prometheus (prom-client) |
| Visualization | Grafana |
| Nodemailer | |
| Testing | Jest |
| Containerization | Docker, Docker Compose |
| CI/CD | GitHub Actions |
sequenceDiagram
participant C as Client
participant A as Auth API
participant R as Redis
participant DB as PostgreSQL
participant Q as BullMQ
C->>A: POST /auth/signup
A->>R: Check rate limit (email + IP)
A->>A: Validate password strength
A->>DB: Check existing email/username
A->>A: Hash password (bcrypt, 12 rounds)
A->>DB: Create user + security record (transaction)
A->>A: Generate 6-digit verification code
A->>R: Store code
A->>Q: Queue verification email
A-->>C: 201 Created
Note over C,Q: User receives email with code
C->>A: POST /auth/verify-email
A->>R: Validate code
A->>DB: Mark user as verified
A->>Q: Queue welcome email
A-->>C: 200 OK (Email verified)
sequenceDiagram
participant C as Client
participant A as Auth API
participant R as Redis
participant DB as PostgreSQL
C->>A: POST /auth/login
A->>R: Check rate limit (email + IP)
A->>DB: Fetch user with security data
alt User not found
A->>A: Run fake bcrypt (timing attack prevention)
A-->>C: 401 Invalid credentials
end
A->>A: Check account status
A->>A: Check account lockout
A->>A: Verify password (bcrypt)
A->>A: Check email verification
A->>R: Acquire distributed lock
A->>A: Generate Access Token (15min)
A->>A: Generate Refresh Token (7d) with JTI
A->>R: Store refresh token hash
A->>R: Track session in user's session list
A->>A: Enforce max devices (revoke oldest if exceeded)
A-->>C: 200 OK (tokens + user info)
sequenceDiagram
participant C as Client
participant A as Auth API
participant R as Redis
participant DB as PostgreSQL
C->>A: POST /auth/refresh
A->>A: Verify refresh token signature
A->>R: Get stored token by JTI
alt Token not found (possibly reused)
A->>R: Revoke ALL user tokens
A-->>C: 401 Token revoked
end
A->>A: Validate token hash
A->>DB: Check user status
Note over A,R: Token Rotation
A->>R: Delete old refresh token
A->>A: Generate new JTI
A->>A: Create new access + refresh tokens
A->>R: Store new refresh token
A-->>C: 200 OK (new tokens)
sequenceDiagram
participant C as Client
participant A as Auth API
participant G as Google OAuth
participant R as Redis
participant DB as PostgreSQL
C->>A: GET /auth/google?redirectUrl=...
A->>A: Generate state token (CSRF protection)
A->>R: Store state with metadata (5 min TTL)
A-->>C: Return Google authorization URL
C->>G: Redirect to Google consent screen
G->>G: User authenticates & consents
G->>A: GET /auth/google/callback?code=...&state=...
A->>R: Validate state token (CSRF check)
A->>G: Exchange code for tokens
G-->>A: Return access_token, id_token
A->>G: Fetch user profile from id_token
alt New User
A->>DB: Create user with provider=google
A->>DB: Mark as verified (Google verified email)
else Existing User
A->>DB: Update last login
end
A->>A: Generate JWT access token
A->>A: Generate refresh token with JTI
A->>R: Store refresh token hash
A-->>C: Redirect to frontend with tokens
sequenceDiagram
participant C as Client
participant G as AuthGuard
participant R as Redis
participant DB as PostgreSQL
participant Ctrl as Controller
C->>G: Request with Authorization
G->>G: Extract token from header
alt No token provided
G-->>C: 401 Unauthorized - No token found
end
G->>G: Verify JWT signature & expiry
alt Invalid/Expired token
G-->>C: 401 Unauthorized - Invalid token
end
G->>G: Extract userId, role, tokenVersion from JWT
Note over G,R: Hybrid Token Validation
G->>R: GET tokenVersion from cache
alt Cache hit
G->>G: Compare tokenVersion
else Cache miss
G->>DB: Fetch user tokenVersion & status
G->>R: Cache tokenVersion
end
alt tokenVersion mismatch
G-->>C: 401 Unauthorized - Token revoked
end
alt User status != ACTIVE
G-->>C: 401 Unauthorized - Account inactive
end
G->>G: Attach user payload to request
G->>Ctrl: Request proceeds to controller
Ctrl-->>C: Response
| Feature | Configuration | File |
|---|---|---|
| Access Token Expiry | 15 minutes | src/auth/config/auth.config.ts |
| Refresh Token Expiry | 7 days | src/auth/config/auth.config.ts |
| Max Login Attempts | 5 per 15 minutes | src/auth/config/auth.config.ts |
| Account Lockout | 30 minutes after max attempts | src/auth/config/auth.config.ts |
| Max Devices per User | 5 simultaneous sessions | src/auth/config/auth.config.ts |
| Password Requirements | Min 8 chars, uppercase, lowercase, number, special char | src/auth/config/auth.config.ts |
| Verification Code Expiry | 24 hours | src/auth/config/auth.config.ts |
To modify authentication settings, edit src/auth/config/auth.config.ts:
export const AUTH_CONFIG = {
// Password Configuration
PASSWORD_MIN_LENGTH: 8,
PASSWORD_REQUIREMENTS: {
UPPERCASE: true,
LOWERCASE: true,
NUMBERS: true,
SPECIAL_CHARS: true,
},
// Token Configuration - Modify expiry times
TOKEN_EXPIRY: {
ACCESS: '15m', // Short-lived for security
REFRESH: '7d', // 7 days
VERIFICATION: '24h',
PASSWORD_RESET: '1h',
},
// Rate Limiting
RATE_LIMIT: {
LOGIN_MAX_ATTEMPTS: 5,
LOGIN_WINDOW_MS: 15 * 60 * 1000, // 15 minutes
},
// Account Lockout
ACCOUNT_LOCKOUT: {
MAX_FAILED_ATTEMPTS: 5,
LOCKOUT_DURATION_MS: 30 * 60 * 1000, // 30 minutes
},
// Session/Device Management
SESSION: {
MAX_DEVICES_PER_USER: 5,
},
} as const;| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/auth |
β | Register new user |
POST |
/auth/verify-email |
β | Verify email with OTP |
POST |
/auth/resend-verification-email |
β | Resend verification email |
POST |
/auth/login |
β | Login and get tokens |
POST |
/auth/refresh-token |
β | Refresh access token |
POST |
/auth/logout |
β | Logout (revoke refresh token) |
POST |
/auth/logout-all |
β | Logout all devices |
GET |
/auth/google |
β | Initiate Google OAuth |
GET |
/auth/google/callback |
β | Google OAuth callback |
POST |
/auth/google/callback |
β | Google OAuth callback (POST) |
GET |
/user |
β* | List all users |
GET |
/user/:id |
β* | Get user by ID |
PATCH |
/user/:id |
β* | Update user |
DELETE |
/user/:id |
β* | Delete user |
GET |
/ |
β | Health check |
GET |
/metrics |
β | Prometheus metrics |
*Note: User endpoints are currently public. See Adding Protected Routes to secure them.
erDiagram
authUser ||--o| authSecurity : has
authUser ||--o| userProfile : has
authUser ||--o{ loginHistory : logs
authUser ||--o{ emailHistory : sends
authUser ||--o{ ActivityLogEvent : tracks
authUser ||--o{ Subscription : subscribes
authUser {
uuid id PK
string email UK
string password
string username UK
enum role "USER|ADMIN|MODERATOR|SUPERADMIN"
boolean verified
enum status "ACTIVE|INACTIVE|SUSPENDED|DELETED|BLOCKED"
int tokenVersion
string provider "local|google"
string providerId
datetime deletedAt
datetime createdAt
datetime updatedAt
}
authSecurity {
uuid id PK
uuid authId FK,UK
int failedAttempts
datetime lastFailedAt
datetime lockExpiresAt
boolean mfaEnabled
enum mfaMethod "totp|sms|email|webauthn"
string mfaSecret
datetime lastPasswordChange
}
userProfile {
uuid id PK
uuid authId FK,UK
string firstName
string lastName
string avatarUrl
}
loginHistory {
uuid id PK
uuid authId FK
enum action "login|logout"
string ipAddress
string userAgent
string device
boolean success
string failureReason
int attemptNumber
datetime createdAt
}
emailHistory {
uuid id PK
uuid authId FK
string emailTo
enum emailType "verification|password_reset|notification"
string subject
string messageId
enum emailStatus "sent|failed|pending|bounced|delivered"
string ipAddress
string userAgent
string errorMessage
datetime sentAt
datetime createdAt
}
ActivityLogEvent {
uuid id PK
uuid authId FK
string entityType
string entityId
enum eventType "create|update|delete|login|logout|password_change"
json changes
string ipAddress
string userAgent
datetime createdAt
}
erDiagram
authUser ||--o{ Subscription : has
SubscriptionPlan ||--o{ Subscription : offers
Subscription ||--o{ Payment : processes
Subscription ||--o{ Invoice : generates
SubscriptionPlan {
uuid id PK
string name UK
string description
decimal price "10,2"
string currency "USD"
enum interval "DAY|WEEK|MONTH|YEAR"
int intervalCount
int trialPeriodDays
json features
boolean isActive
}
Subscription {
uuid id PK
uuid authId FK
uuid planId FK
enum status "ACTIVE|PAST_DUE|CANCELED|TRIALING"
datetime currentPeriodStart
datetime currentPeriodEnd
boolean cancelAtPeriodEnd
datetime canceledAt
datetime trialStart
datetime trialEnd
}
Payment {
uuid id PK
uuid subscriptionId FK
decimal amount "10,2"
string currency
enum status "PENDING|PROCESSING|SUCCEEDED|FAILED|REFUNDED"
string provider "stripe|paypal"
string providerPaymentId
json metadata
}
Invoice {
uuid id PK
uuid subscriptionId FK
string invoiceNumber UK
decimal amount "10,2"
string currency
enum status "DRAFT|OPEN|PAID|VOID"
datetime dueDate
datetime paidAt
string invoiceUrl
}
// User Roles
enum userRole {
USER
ADMIN
MODERATOR
SUPERADMIN
}
// User Status
enum userStatus {
ACTIVE
INACTIVE
SUSPENDED
DELETED
BLOCKED
}
// Subscription Status
enum subscriptionStatus {
ACTIVE
PAST_DUE
CANCELED
TRIALING
INCOMPLETE
INCOMPLETE_EXPIRED
UNPAID
}
// Billing Interval
enum billingInterval {
DAY
WEEK
MONTH
YEAR
}# Create a new migration
npx prisma migrate dev --name your_migration_name
# Apply migrations in production
npx prisma migrate deploy
# Reset database (development only)
npx prisma migrate reset
# Generate Prisma Client
npx prisma generatenestjs-prisma-postgres-starter/
βββ π src/
β βββ π auth/ # Authentication module
β β βββ π config/ # Auth configuration (timeouts, limits)
β β βββ π dto/ # Data transfer objects
β β βββ π interfaces/ # TypeScript interfaces
β β βββ π services/ # Auth utility services
β β βββ auth.controller.ts # Auth endpoints
β β βββ auth.service.ts # Core auth business logic
β β βββ auth.module.ts # Module definition
β β
β βββ π common/ # Shared utilities
β β βββ π config/ # App & Winston configuration
β β βββ π dto/ # Shared DTOs
β β βββ π errors/ # Custom error classes
β β βββ π filters/ # Exception filters
β β βββ π guards/ # Auth guards
β β βββ π interceptors/ # Response & metrics interceptors
β β βββ π modules/ # Logger, Redis, Queue modules
β β βββ π queues/ # BullMQ email queue
β β βββ π services/ # Prisma, Redis, Logger services
β β
β βββ π metrics/ # Prometheus metrics
β β βββ metrics.controller.ts # /metrics endpoint
β β βββ metrics.service.ts # Metric definitions
β β βββ metrics.interceptor.ts # Request tracking
β β
β βββ π user/ # User management module
β β βββ π dto/ # User DTOs
β β βββ user.controller.ts # User endpoints
β β βββ user.service.ts # User business logic
β β
β βββ app.module.ts # Root module
β βββ app.controller.ts # Health check endpoint
β βββ main.ts # Application bootstrap
β
βββ π prisma/
β βββ π schema/ # Modular Prisma schemas
β β βββ base.prisma # Generator & datasource config
β β βββ enums.prisma # All enums
β β βββ auth.prisma # AuthUser, AuthSecurity models
β β βββ profile.prisma # UserProfile model
β β βββ history.prisma # LoginHistory, EmailHistory
β β βββ activityLog.prisma # Activity logging
β β βββ subscription.prisma # Subscription, Payment, Invoice
β βββ π migrations/ # Database migrations
β
βββ π monitoring/
β βββ π prometheus/ # Prometheus config
β βββ π grafana/ # Grafana provisioning
β βββ π loki/ # Loki configuration
β
βββ π templates/
β βββ π emails/ # Email HTML templates
β
βββ π test/ # E2E tests
βββ docker-compose.yaml # Development services
βββ docker-compose.prod.yaml # Production backend
βββ docker-compose.override.yaml # Development overrides
βββ Dockerfile # Multi-stage build
βββ .env.example # Environment template
βββ postman-collection.json # API collection
- Node.js β₯ 22
- npm β₯ 10
- Docker & Docker Compose
- Git
-
Clone the repository
git clone https://github.com/the-pujon/nestjs-prisma-postgres-starter.git cd nestjs-prisma-postgres-starter -
Create environment file
cp .env.example .env
-
Update
.envwith your configuration (see Environment Configuration) -
Start Docker services
# Start all services (PostgreSQL, Redis, Prometheus, Grafana, Loki) docker compose up -d -
Install dependencies
npm install
-
Run database migrations
npx prisma migrate dev
-
Generate Prisma client
npx prisma generate
-
Start the application
# Development mode with hot reload npm run start:dev # Or production mode npm run build && npm run start:prod
| Script | Description |
|---|---|
npm run start:dev |
Start in development mode with hot reload |
npm run start:debug |
Start in debug mode with inspector |
npm run start:prod |
Start in production mode |
npm run build |
Build the application |
npm run test |
Run unit tests |
npm run test:watch |
Run tests in watch mode |
npm run test:cov |
Run tests with coverage report |
npm run test:e2e |
Run end-to-end tests |
npm run lint |
Run ESLint and fix issues |
npm run format |
Format code with Prettier |
npm run docker:dev |
Start Docker services |
| Command | Description |
|---|---|
npx prisma generate |
Generate Prisma Client |
npx prisma migrate dev |
Create and apply new migration |
npx prisma migrate deploy |
Apply pending migrations (production) |
npx prisma migrate reset |
Reset database and apply all migrations |
npx prisma studio |
Open Prisma Studio GUI |
npx prisma db push |
Push schema changes without migration |
- Verify installation
- API: http://localhost:5000
- PgAdmin: http://localhost:8080
- RedisInsight: http://localhost:8001
- Prometheus: http://localhost:9090
- Grafana: http://localhost:3000
- Loki: http://localhost:3100
This project includes automatic Swagger/OpenAPI documentation with zero configuration required.
Once the application is running, access the interactive API documentation at:
π http://localhost:5000/docs
- β Zero decorators required - DTOs are automatically documented
- β Global response format - Consistent API responses
- β JWT authentication - Bearer token support built-in
- β Try it out - Test endpoints directly from the browser
- β Pagination support - Standard pagination patterns included
For detailed information, see docs/SWAGGER.md
| Variable | Required | Default | Description |
|---|---|---|---|
| Database | |||
DATABASE_URL |
β | - | PostgreSQL connection string |
POSTGRES_USER |
β | - | PostgreSQL username (for Docker) |
POSTGRES_PASSWORD |
β | - | PostgreSQL password (for Docker) |
POSTGRES_DB |
β | - | PostgreSQL database name |
DATABASE_PORT |
β | 5433 |
PostgreSQL port mapping |
DATABASE_HOST |
β | 127.0.0.1 |
PostgreSQL host |
| Application | |||
NODE_ENV |
β | development |
Environment: development, production, test |
PORT |
β | 5000 |
Application port |
| JWT Authentication | |||
JWT_SECRET |
β | - | JWT signing secret (min 256 bits) |
JWT_ACCESS_SECRET |
β | JWT_SECRET |
Separate secret for access tokens |
JWT_REFRESH_SECRET |
β | JWT_SECRET |
Separate secret for refresh tokens |
| Redis | |||
REDIS_HOST |
β | localhost |
Redis server host |
REDIS_PORT |
β | 6379 |
Redis server port |
REDIS_USER |
β | default |
Redis username |
REDIS_PASSWORD |
β | - | Redis password |
REDIS_DB |
β | 0 |
Redis database number |
REDIS_CACHE_KEY_PREFIX |
β | app |
Prefix for all Redis keys |
| Email (SMTP) | |||
EMAIL_HOST |
β | smtp.gmail.com |
SMTP server host |
EMAIL_PORT |
β | 587 |
SMTP server port |
EMAIL_USER |
β | - | SMTP username (email address) |
EMAIL_PASS |
β | - | SMTP password (App Password for Gmail) |
EMAIL_FROM |
β | EMAIL_USER |
From address for outgoing emails |
| Google OAuth | |||
GOOGLE_CLIENT_ID |
β | - | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
β | - | Google OAuth client secret |
GOOGLE_REDIRECT_URI |
β | - | OAuth callback URL |
| Monitoring | |||
GRAFANA_ADMIN_USER |
β | admin |
Grafana admin username |
GRAFANA_ADMIN_PASSWORD |
β | admin |
Grafana admin password |
LOKI_ENABLED |
β | true |
Enable Loki log aggregation |
LOKI_URL |
β | http://localhost:3100 |
Loki server URL |
| PgAdmin | |||
PGADMIN_DEFAULT_EMAIL |
β | admin@example.com |
PgAdmin login email |
PGADMIN_DEFAULT_PASSWORD |
β | admin |
PgAdmin login password |
Create a .env file based on .env.example:
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# DATABASE CONFIGURATION
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
DATABASE_URL=postgresql://admin:admin@127.0.0.1:5433/simple_blog
POSTGRES_USER=admin
POSTGRES_PASSWORD=admin
POSTGRES_DB=simple_blog
DATABASE_PORT=5433
DATABASE_HOST=127.0.0.1
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# APPLICATION CONFIGURATION
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
NODE_ENV=development
PORT=5000
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# JWT CONFIGURATION
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# IMPORTANT: Use a strong, random secret (at least 256 bits)
# Generate with: openssl rand -base64 32
JWT_SECRET=a-string-secret-at-least-256-bits-long
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# REDIS CONFIGURATION
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_USER=default
REDIS_PASSWORD=your_redis_password
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# EMAIL CONFIGURATION (Gmail SMTP)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your-email@gmail.com
EMAIL_PASS=your-app-password # Use Gmail App Password
EMAIL_FROM=noreply@yourapp.com
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# GOOGLE OAUTH (Optional)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Get credentials: https://console.cloud.google.com/apis/credentials
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:5000/auth/google/callback
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PGADMIN CONFIGURATION
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PGADMIN_DEFAULT_EMAIL=admin@example.com
PGADMIN_DEFAULT_PASSWORD=admin
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# MONITORING CONFIGURATION
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
LOKI_ENABLED=true
LOKI_URL=http://localhost:3100- Enable 2-Factor Authentication on your Google account
- Go to Google App Passwords
- Generate a new app password for "Mail"
- Use this password in
EMAIL_PASS
Development: http://localhost:5000
Production: https://your-domain.com
All API responses follow a consistent format:
// Success Response
{
"statusCode": 200,
"message": "Success",
"data": { ... }
}
// Error Response
{
"statusCode": 400,
"message": "Error description",
"errors": { ... },
"error": "AppError"
}POST /auth
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePass123!",
"username": "johndoe"
}Password Requirements:
- Minimum 8 characters
- At least 1 uppercase letter
- At least 1 lowercase letter
- At least 1 number
- At least 1 special character
Response (201 Created):
{
"statusCode": 201,
"message": "Success",
"data": null
}A verification email with 6-digit code will be sent to the user.
POST /auth/verify-email
Content-Type: application/json
{
"email": "user@example.com",
"code": "123456"
}Response (200 OK):
{
"statusCode": 200,
"message": "Success",
"data": {
"message": "Email verified successfully"
}
}POST /auth/resend-verification-email
Content-Type: application/json
{
"email": "user@example.com"
}Rate Limit: Max 3 requests per 15 minutes per email.
POST /auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePass123!"
}Response (200 OK):
{
"statusCode": 200,
"message": "Success",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "johndoe",
"role": "USER",
"verified": true
},
"expiresIn": 900
}
}Rate Limit: Max 5 attempts per 15 minutes per email/IP.
POST /auth/refresh-token
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Response (200 OK):
{
"statusCode": 200,
"message": "Success",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 900
}
}Note: Token rotation is implemented. The old refresh token is invalidated, and a new one is issued.
POST /auth/logout
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"userId": "550e8400-e29b-41d4-a716-446655440000"
}Response (200 OK):
{
"statusCode": 200,
"message": "Success",
"data": {
"success": true,
"message": "Logged out successfully"
}
}POST /auth/logout-all
Content-Type: application/json
{
"userId": "550e8400-e29b-41d4-a716-446655440000"
}Response (200 OK):
{
"statusCode": 200,
"message": "Success",
"data": {
"success": true,
"message": "Logged out from all devices successfully"
}
}Note: This also increments the
tokenVersion, immediately invalidating all existing access tokens.
GET /auth/google?redirectUrl=http://localhost:3000/dashboardResponse (200 OK):
{
"statusCode": 200,
"message": "Success",
"data": {
"url": "https://accounts.google.com/o/oauth2/v2/auth?...",
"state": "abc123...",
"message": "Redirect to the provided URL to authenticate with Google"
}
}GET /auth/google/callback?code=AUTH_CODE&state=STATE_TOKENBrowser Flow: Redirects to redirectUrl with tokens as query params:
http://localhost:3000/dashboard?access_token=xxx&refresh_token=xxx&user_id=xxx&email=xxx&is_new_user=false
API Flow (POST):
POST /auth/google/callback
Content-Type: application/json
{
"code": "AUTH_CODE",
"state": "STATE_TOKEN"
}Response (200 OK):
{
"statusCode": 200,
"message": "Success",
"data": {
"success": true,
"message": "Signed in successfully via Google",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@gmail.com",
"username": "user_google_123456"
},
"isNewUser": true
}
}
}GET /userGET /user/:idPATCH /user/:id
Content-Type: application/json
{
"username": "newusername"
}DELETE /user/:idGET /Response:
{
"statusCode": 200,
"message": "Success",
"data": "Hello World!"
}GET /metricsResponse: Prometheus text format metrics
Import postman-collection.json into Postman for a complete API testing environment with examples.
curl -X POST http://localhost:5000/auth/signup \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePass123!",
"username": "johndoe"
}'curl -X POST http://localhost:5000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePass123!"
}'curl -X GET http://localhost:5000/user/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"The template includes a pre-built AuthGuard that validates JWT tokens with hybrid Redis/DB verification. Here's how to protect your routes:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../common/guards/auth.guard';
@Controller('products')
@UseGuards(AuthGuard) // All routes in this controller require authentication
export class ProductController {
@Get()
findAll() {
return this.productService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.productService.findOne(id);
}
}import { Controller, Get, Post, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '../common/guards/auth.guard';
@Controller('products')
export class ProductController {
@Get() // Public route - no guard
findAll() {
return this.productService.findAll();
}
@Post()
@UseGuards(AuthGuard) // Protected route - requires authentication
create(@Body() dto: CreateProductDto, @Request() req) {
// Access authenticated user from request
const userId = req.user.userId;
const userRole = req.user.role;
return this.productService.create(dto, userId);
}
}When a route is protected, the AuthGuard attaches the user payload to the request:
@Get('profile')
@UseGuards(AuthGuard)
getProfile(@Request() req) {
// req.user contains: { userId, role, tokenVersion }
console.log(req.user.userId); // User's UUID
console.log(req.user.role); // 'USER' | 'ADMIN' | 'MODERATOR' | 'SUPERADMIN'
return this.userService.findById(req.user.userId);
}For cleaner code, create a custom decorator:
// src/common/decorators/user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
if (data) {
return request.user?.[data];
}
return request.user;
},
);
// Usage in controller
@Get('profile')
@UseGuards(AuthGuard)
getProfile(@CurrentUser() user) {
return this.userService.findById(user.userId);
}
@Get('my-orders')
@UseGuards(AuthGuard)
getMyOrders(@CurrentUser('userId') userId: string) {
return this.orderService.findByUser(userId);
}Create a roles guard for admin-only routes:
// src/common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
}
// Create roles decorator
// src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// Usage
@Get('admin/users')
@UseGuards(AuthGuard, RolesGuard)
@Roles('ADMIN', 'SUPERADMIN')
getAllUsers() {
return this.userService.findAll();
}Let's add a complete Product module as an example:
# Generate module, controller, and service
nest g module product
nest g controller product
nest g service productCreate prisma/schema/product.prisma:
model Product {
id String @id @default(uuid())
name String
description String?
price Decimal @db.Decimal(10, 2)
stock Int @default(0)
isActive Boolean @default(true)
createdBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
creator authUser @relation(fields: [createdBy], references: [id])
@@index([name])
@@index([createdBy])
}Don't forget to add the relation in auth.prisma:
model authUser {
// ... existing fields
products Product[]
}npx prisma migrate dev --name add_product_modelCreate src/product/dto/create-product.dto.ts:
import { IsString, IsNumber, IsOptional, Min, MaxLength } from 'class-validator';
export class CreateProductDto {
@IsString()
@MaxLength(200)
name: string;
@IsString()
@IsOptional()
@MaxLength(2000)
description?: string;
@IsNumber({ maxDecimalPlaces: 2 })
@Min(0)
price: number;
@IsNumber()
@Min(0)
@IsOptional()
stock?: number;
}// src/product/product.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../common/services/prisma.service';
import { CustomLoggerService } from '../common/services/custom-logger.service';
import { ActivityLogService } from '../common/services/activity-log.service';
import { CreateProductDto } from './dto/create-product.dto';
import AppError from '../common/errors/app.error';
@Injectable()
export class ProductService {
constructor(
private readonly prisma: PrismaService,
private readonly logger: CustomLoggerService,
private readonly activityLog: ActivityLogService,
) {}
async create(dto: CreateProductDto, userId: string, meta: { ip: string; userAgent: string }) {
this.logger.log(`Creating product: ${dto.name}`, 'ProductService');
const product = await this.prisma.$transaction(async (tx) => {
const created = await tx.product.create({
data: {
...dto,
createdBy: userId,
},
});
// Log activity
await this.activityLog.logCreate(
'product',
created.id,
{ name: dto.name, price: dto.price },
{ ip: meta.ip, userAgent: meta.userAgent, actionedBy: userId },
tx,
);
return created;
});
return product;
}
async findAll(page = 1, limit = 10) {
const skip = (page - 1) * limit;
const [products, total] = await Promise.all([
this.prisma.product.findMany({
where: { isActive: true },
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
this.prisma.product.count({ where: { isActive: true } }),
]);
return {
data: products,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
async findOne(id: string) {
const product = await this.prisma.product.findUnique({
where: { id },
});
if (!product) {
throw AppError.notFound('Product not found');
}
return product;
}
}// src/product/product.controller.ts
import { Controller, Get, Post, Body, Param, Query, UseGuards, Request } from '@nestjs/common';
import { ProductService } from './product.service';
import { CreateProductDto } from './dto/create-product.dto';
import { AuthGuard } from '../common/guards/auth.guard';
@Controller('products')
export class ProductController {
constructor(private readonly productService: ProductService) {}
@Post()
@UseGuards(AuthGuard)
create(@Body() dto: CreateProductDto, @Request() req) {
const meta = {
ip: req.ip || 'unknown',
userAgent: req.headers['user-agent'] || 'unknown',
};
return this.productService.create(dto, req.user.userId, meta);
}
@Get()
findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
return this.productService.findAll(+page, +limit);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.productService.findOne(id);
}
}// src/product/product.module.ts
import { Module } from '@nestjs/common';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';
import { PrismaService } from '../common/services/prisma.service';
import { CustomLoggerService } from '../common/services/custom-logger.service';
import { ActivityLogService } from '../common/services/activity-log.service';
import { RedisService } from '../common/services/redis.service';
@Module({
controllers: [ProductController],
providers: [
ProductService,
PrismaService,
CustomLoggerService,
ActivityLogService,
RedisService,
],
})
export class ProductModule {}Use the built-in RedisService for caching:
import { Injectable } from '@nestjs/common';
import { RedisService } from '../common/services/redis.service';
import { PrismaService } from '../common/services/prisma.service';
@Injectable()
export class ProductService {
constructor(
private readonly redis: RedisService,
private readonly prisma: PrismaService,
) {}
async findOne(id: string) {
const cacheKey = `product:${id}`;
// Try cache first
const cached = await this.redis.get(cacheKey);
if (cached) {
return cached;
}
// Fetch from database
const product = await this.prisma.product.findUnique({
where: { id },
});
if (product) {
// Cache for 1 hour
await this.redis.set(cacheKey, product, 3600);
}
return product;
}
async update(id: string, data: any) {
const product = await this.prisma.product.update({
where: { id },
data,
});
// Invalidate cache
await this.redis.del(`product:${id}`);
return product;
}
}Use the existing BullMQ setup to add new job types:
// src/common/queues/notification/notification.queue.ts
import { InjectQueue } from '@nestjs/bullmq';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bullmq';
@Injectable()
export class NotificationQueueService {
constructor(@InjectQueue('notification') private notificationQueue: Queue) {}
async sendPushNotification(userId: string, title: string, body: string) {
await this.notificationQueue.add(
'push-notification',
{ userId, title, body },
{
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
},
);
}
}Register the queue in src/common/modules/queue.module.ts:
@Module({
imports: [
BullModule.registerQueue(
{ name: 'email' },
{ name: 'notification' }, // Add new queue
),
],
// ...
})The processor picks jobs from the queue and processes them:
// src/common/queues/notification/notification.processor.ts
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Inject } from '@nestjs/common';
import { Job } from 'bullmq';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
// Define job data interface
export interface PushNotificationJob {
userId: string;
title: string;
body: string;
}
@Processor('notification') // Must match queue name
export class NotificationProcessor extends WorkerHost {
constructor(
@Inject(WINSTON_MODULE_PROVIDER)
private readonly logger: Logger,
) {
super();
}
// This method is called automatically when a job is picked from the queue
async process(job: Job<PushNotificationJob>): Promise<void> {
this.logger.info(`Processing notification job: ${job.name} (ID: ${job.id})`, {
context: 'NotificationProcessor',
jobId: job.id,
jobName: job.name,
});
try {
const { userId, title, body } = job.data;
// Your notification logic here
await this.sendPushNotification(userId, title, body);
this.logger.info(`Notification sent successfully to user ${userId}`, {
context: 'NotificationProcessor',
jobId: job.id,
});
} catch (error) {
this.logger.error(`Failed to process notification job ${job.id}`, {
context: 'NotificationProcessor',
jobId: job.id,
error: error instanceof Error ? error.message : String(error),
});
// Re-throw to trigger retry (based on job config)
throw error;
}
}
private async sendPushNotification(
userId: string,
title: string,
body: string,
): Promise<void> {
// Implement your push notification logic
// e.g., Firebase Cloud Messaging, OneSignal, etc.
console.log(`Sending push to ${userId}: ${title} - ${body}`);
}
}// src/common/modules/queue.module.ts
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bullmq';
import { NotificationQueueService } from '../queues/notification/notification.queue';
import { NotificationProcessor } from '../queues/notification/notification.processor';
@Module({
imports: [
BullModule.forRoot({
connection: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
},
}),
BullModule.registerQueue(
{ name: 'email' },
{ name: 'notification' },
),
],
providers: [
NotificationQueueService,
NotificationProcessor, // Register the processor
],
exports: [NotificationQueueService],
})
export class QueueModule {}// In any service
import { NotificationQueueService } from '../common/queues/notification/notification.queue';
@Injectable()
export class OrderService {
constructor(
private readonly notificationQueue: NotificationQueueService,
) {}
async createOrder(data: CreateOrderDto, userId: string) {
const order = await this.prisma.order.create({ data });
// Queue notification (processed in background)
await this.notificationQueue.sendPushNotification(
userId,
'Order Confirmed!',
`Your order #${order.id} has been placed.`,
);
return order;
}
}await this.queue.add('job-name', jobData, {
attempts: 3, // Retry 3 times on failure
backoff: {
type: 'exponential', // exponential, fixed
delay: 2000, // Initial delay: 2s, then 4s, 8s...
},
delay: 5000, // Delay job execution by 5 seconds
priority: 1, // Lower = higher priority
removeOnComplete: 100, // Keep last 100 completed jobs
removeOnFail: 500, // Keep last 500 failed jobs
timeout: 30000, // Job timeout: 30 seconds
});- Open http://localhost:8001
- Browse keys starting with
bull:notification:bull:notification:waiting- Jobs waiting to be processedbull:notification:active- Currently processingbull:notification:completed- Finished jobsbull:notification:failed- Failed jobs
The template includes a built-in ActivityLogService for audit trails:
import { ActivityLogService } from '../common/services/activity-log.service';
@Injectable()
export class ProductService {
constructor(
private readonly activityLog: ActivityLogService,
private readonly prisma: PrismaService,
) {}
async update(id: string, data: UpdateProductDto, userId: string, meta: { ip: string; userAgent: string }) {
const oldProduct = await this.prisma.product.findUnique({ where: { id } });
const updated = await this.prisma.$transaction(async (tx) => {
const product = await tx.product.update({
where: { id },
data,
});
// Log the update with field changes
await this.activityLog.logUpdate(
'product',
id,
{
name: { old: oldProduct.name, new: data.name },
price: { old: oldProduct.price, new: data.price },
},
{ ip: meta.ip, userAgent: meta.userAgent, actionedBy: userId },
tx,
);
return product;
});
return updated;
}
}Use the Prometheus metrics service to track custom business metrics:
import { MetricsService } from '../metrics/metrics.service';
@Injectable()
export class OrderService {
constructor(private readonly metrics: MetricsService) {}
async createOrder(data: CreateOrderDto) {
const order = await this.prisma.order.create({ data });
// Track custom metrics
this.metrics.recordDatabaseQuery('create', 'Order');
return order;
}
}To add a new email type (e.g., order confirmation):
Create templates/emails/order-confirmation.html:
<!DOCTYPE html>
<html>
<head>
<style>
.container { max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif; }
.header { background: #4F46E5; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; }
.order-details { background: #f5f5f5; padding: 15px; border-radius: 5px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Order Confirmed!</h1>
</div>
<div class="content">
<p>Hi {{username}},</p>
<p>Your order <strong>#{{orderId}}</strong> has been confirmed.</p>
<div class="order-details">
<p><strong>Order Total:</strong> ${{total}}</p>
<p><strong>Estimated Delivery:</strong> {{deliveryDate}}</p>
</div>
</div>
</div>
</body>
</html>// src/common/queues/email/email.queue.ts
export interface OrderConfirmationEmailJob {
type: 'order-confirmation';
email: string;
username: string;
orderId: string;
total: string;
deliveryDate: string;
}
export type EmailJob = VerificationEmailJob | WelcomeEmailJob | OrderConfirmationEmailJob;
// Add method to EmailQueueService
async sendOrderConfirmationEmail(
email: string,
username: string,
orderId: string,
total: string,
deliveryDate: string,
): Promise<void> {
await this.emailQueue.add(
'send-order-confirmation',
{
type: 'order-confirmation',
email,
username,
orderId,
total,
deliveryDate,
} as OrderConfirmationEmailJob,
{
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: 100,
removeOnFail: 500,
},
);
}Update the email processor to handle the new job type.
All errors follow this consistent format:
{
"statusCode": 400,
"message": "Human-readable error message",
"errors": {
"code": "ERROR_CODE",
"details": {}
},
"error": "AppError"
}| HTTP Status | Error Type | When It Occurs |
|---|---|---|
400 |
Bad Request | Invalid input, validation failed, weak password |
401 |
Unauthorized | Invalid/expired token, wrong credentials |
403 |
Forbidden | Email not verified, account locked/suspended |
404 |
Not Found | User/resource doesn't exist |
409 |
Conflict | Email/username already exists, concurrent login |
429 |
Too Many Requests | Rate limit exceeded |
500 |
Internal Server Error | Unexpected server error |
503 |
Service Unavailable | Redis/DB connection failed |
// Invalid credentials
{
"statusCode": 401,
"message": "Invalid email or password"
}
// Token expired
{
"statusCode": 401,
"message": "Token has expired"
}
// Token revoked (password changed, force logout)
{
"statusCode": 401,
"message": "Token has been revoked. Please login again."
}
// Account locked
{
"statusCode": 403,
"message": "Account is temporarily locked. Please try again in 25 minutes."
}
// Email not verified
{
"statusCode": 403,
"message": "Please verify your email address before logging in"
}
// Account suspended
{
"statusCode": 403,
"message": "Your account has been suspended. Please contact support."
}// Weak password
{
"statusCode": 400,
"message": "Password does not meet security requirements"
}
// Invalid input
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{ "property": "email", "constraints": { "isEmail": "email must be an email" } }
]
}{
"statusCode": 429,
"message": "Too Many Requests"
}try {
await api.login(email, password);
} catch (error) {
if (error.statusCode === 401) {
setError('Invalid email or password');
} else if (error.statusCode === 403) {
if (error.message.includes('verify')) {
// Redirect to email verification
router.push('/verify-email');
} else if (error.message.includes('locked')) {
setError('Account locked. Please try again later.');
}
} else if (error.statusCode === 429) {
setError('Too many attempts. Please wait 15 minutes.');
} else {
setError('An unexpected error occurred');
}
}# Start all services
docker compose up -d
# Start specific services only
docker compose up -d postgres_db redis-stack
# View logs
docker compose logs -f
# View logs for specific service
docker compose logs -f postgres_db
# Stop services
docker compose down
# Stop and remove volumes (reset all data)
docker compose down -v
# Rebuild containers
docker compose up -d --build| Service | Port(s) | URL | Description |
|---|---|---|---|
postgres_db |
5433 | - | PostgreSQL 17 database |
pg_admin |
8080, 8443 | http://localhost:8080 | PgAdmin web interface |
redis-stack |
6379, 8001 | http://localhost:8001 | Redis Stack with RedisInsight |
prometheus |
9090 | http://localhost:9090 | Metrics collection |
grafana |
3000 | http://localhost:3000 | Visualization dashboards |
loki |
3100 | http://localhost:3100 | Log aggregation |
| Service | Username | Password |
|---|---|---|
| PostgreSQL | admin (or POSTGRES_USER) |
admin (or POSTGRES_PASSWORD) |
| PgAdmin | admin@example.com |
admin |
| Grafana | admin |
admin |
| Redis | default |
(no password by default) |
- Open http://localhost:8080
- Login with
admin@example.com/admin - Right-click "Servers" β "Register" β "Server"
- General tab: Name:
Local Development - Connection tab:
- Host:
postgres_db(Docker network name) - Port:
5432(internal port) - Database:
simple_blog - Username:
admin - Password:
admin
- Host:
- Open http://localhost:8001
- Click "Add Redis Database"
- Use connection:
redis://localhost:6379 - Browse keys, monitor commands, view memory usage
# Build production Docker image
docker build -t your-app:latest .
# Run production container
docker run -d \
--name your-app \
-p 5000:5000 \
--env-file .env.production \
your-app:latestThe Dockerfile uses a multi-stage build for optimized production images:
# Build Stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY prisma ./prisma/
RUN npm ci
COPY . .
RUN npx prisma generate
RUN npm run build
# Runtime Stage (smaller image)
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma
COPY package*.json ./
ENV NODE_ENV=production
EXPOSE 5000
CMD ["node", "dist/src/main.js"]The template includes a complete observability stack:
flowchart LR
subgraph Application
A[NestJS App]
W[Winston Logger]
end
subgraph Metrics Pipeline
P[(Prometheus)]
end
subgraph Logs Pipeline
L[(Loki)]
end
subgraph Visualization
G[Grafana Dashboard]
end
A -->|metrics| P
W -->|logs| L
P --> G
L --> G
Data Flow:
- Metrics: NestJS exposes
/metricsendpoint β Prometheus scrapes every 15s β Grafana visualizes - Logs: Winston sends structured logs β Loki stores with labels β Grafana queries with LogQL
The template provides a structured logging service:
import { CustomLoggerService } from '../common/services/custom-logger.service';
@Injectable()
export class YourService {
constructor(private readonly logger: CustomLoggerService) {}
async someMethod() {
// Basic logging
this.logger.log('Operation started', 'YourService');
this.logger.warn('Warning message', 'YourService');
this.logger.error('Error occurred', 'stack trace', 'YourService');
// Structured logging with metadata
this.logger.logUserAction(userId, 'ORDER_PLACED', {
orderId: '123',
total: 99.99,
});
// Log API requests
this.logger.logApiRequest('POST', '/orders', 201, 145); // 145ms
// Log database queries
this.logger.logDatabaseQuery('findMany', 'Order', 23, true); // 23ms, success
}
}Access metrics at: http://localhost:5000/metrics
Available Metrics:
http_request_duration_seconds- Request latency histogramhttp_requests_total- Total request count by method/route/statushttp_request_errors_total- Error count by typeactive_users_total- Currently active users- Default Node.js metrics (CPU, memory, event loop)
- Access Grafana at http://localhost:3000
- Login with
admin/admin(or your configured credentials) - Data sources (Prometheus, Loki) are auto-provisioned
Winston automatically sends logs to Loki with labels:
app: nestjs-appenvironment: development|production
Query logs in Grafana with LogQL:
{app="nestjs-app"} |= "error"
The workflow (.github/workflows/deploy.yaml) automates deployment to EC2:
name: CI/CD Deploy to EC2
on:
push:
branches:
- deploy
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build Docker Image
run: docker build -t your-username/app-name:latest .
- name: Push Docker Image
run: docker push your-username/app-name:latest
- name: Deploy to EC2
uses: appleboy/ssh-action@v0.1.6
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
cd /home/ec2-user/my-app
docker-compose -f docker-compose.yaml -f docker-compose.prod.yaml pull
docker-compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d| Secret | Description |
|---|---|
DOCKER_USERNAME |
Docker Hub username |
DOCKER_PASSWORD |
Docker Hub access token |
EC2_HOST |
EC2 public IP or domain |
EC2_USER |
EC2 SSH username (e.g., ec2-user) |
EC2_SSH_KEY |
Private SSH key for EC2 |
# Unit tests
npm run test
# Watch mode
npm run test:watch
# Coverage report
npm run test:cov
# E2E tests
npm run test:e2esrc/
βββ auth/
β βββ auth.controller.spec.ts # Controller tests
β βββ auth.service.spec.ts # Service tests
βββ user/
β βββ user.controller.spec.ts
β βββ user.service.spec.ts
βββ app.controller.spec.ts
test/
βββ app.e2e-spec.ts # End-to-end tests
βββ jest-e2e.json # E2E Jest config
// src/product/product.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ProductService } from './product.service';
import { PrismaService } from '../common/services/prisma.service';
import { mockDeep, DeepMockProxy } from 'jest-mock-extended';
describe('ProductService', () => {
let service: ProductService;
let prisma: DeepMockProxy<PrismaService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ProductService,
{ provide: PrismaService, useValue: mockDeep<PrismaService>() },
],
}).compile();
service = module.get<ProductService>(ProductService);
prisma = module.get(PrismaService);
});
describe('findOne', () => {
it('should return a product when found', async () => {
const mockProduct = { id: '1', name: 'Test', price: 100 };
prisma.product.findUnique.mockResolvedValue(mockProduct);
const result = await service.findOne('1');
expect(result).toEqual(mockProduct);
expect(prisma.product.findUnique).toHaveBeenCalledWith({
where: { id: '1' },
});
});
it('should throw NotFound when product does not exist', async () => {
prisma.product.findUnique.mockResolvedValue(null);
await expect(service.findOne('999')).rejects.toThrow('Product not found');
});
});
});// test/auth.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Auth (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('/auth (POST) - Registration', () => {
it('should register a new user', () => {
return request(app.getHttpServer())
.post('/auth')
.send({
email: 'test@example.com',
password: 'SecurePass123!',
username: 'testuser',
})
.expect(201);
});
it('should reject weak password', () => {
return request(app.getHttpServer())
.post('/auth')
.send({
email: 'test2@example.com',
password: '123',
username: 'testuser2',
})
.expect(400);
});
});
});# SSH into EC2
ssh -i your-key.pem ec2-user@your-ec2-ip
# Install Docker
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Create app directory
mkdir -p /home/ec2-user/my-app
cd /home/ec2-user/my-appCreate .env on server with production values:
- Use strong, unique
JWT_SECRET - Set
NODE_ENV=production - Configure production database credentials
- Set up proper email configuration
# Copy docker-compose files to server
scp docker-compose.yaml docker-compose.prod.yaml ec2-user@server:/home/ec2-user/my-app/
# Pull and run
docker-compose -f docker-compose.yaml -f docker-compose.prod.yaml pull
docker-compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d
# Run migrations
docker exec -it simple_blog_backend npx prisma migrate deployπ§ Coming Soon - Detailed Nginx reverse proxy configuration with Let's Encrypt SSL certificates will be added in a future update.
Error: Can't reach database server at localhost:5433
Solutions:
# Check if PostgreSQL is running
docker ps | grep postgres
# Restart PostgreSQL
docker compose restart postgres_db
# Check DATABASE_URL in .env matches docker-compose ports
# Default: postgresql://admin:admin@127.0.0.1:5433/simple_blogError: Redis connection to localhost:6379 failed
Solutions:
# Check if Redis is running
docker ps | grep redis
# Restart Redis
docker compose restart redis-stack
# Verify Redis env variables
REDIS_HOST=localhost
REDIS_PORT=6379Error: Migration failed to apply
Solutions:
# Reset database (WARNING: deletes all data)
npx prisma migrate reset
# Generate Prisma client
npx prisma generate
# Check migration status
npx prisma migrate statusError: Verification emails not received
Solutions:
- Check Gmail App Password (not regular password)
- Verify EMAIL_* env variables are set
- Check email job in Redis:
# Open RedisInsight at http://localhost:8001 # Look for keys starting with "bull:email:"
- Check application logs for email errors
Error: Token has expired or Invalid token
Causes & Solutions:
| Error | Cause | Solution |
|---|---|---|
| Token expired | Access token > 15min old | Use refresh token to get new access token |
| Invalid token | Secret mismatch | Ensure JWT_SECRET is same across restarts |
| Token revoked | User logged out or password changed | Login again |
Error: Port 5000 is already in use
Solutions:
# Find process using port
lsof -i :5000
# Kill process
kill -9 <PID>
# Or change port in .env
PORT=5001Error: @prisma/client did not initialize yet
Solutions:
# Generate Prisma client
npx prisma generate
# Or reinstall dependencies
rm -rf node_modules
npm installError: Container keeps restarting
Solutions:
# Increase Docker memory limit
# Docker Desktop > Settings > Resources > Memory: 4GB+
# Or reduce services
docker compose up -d postgres_db redis-stack
# Start monitoring stack later
docker compose up -d prometheus grafana lokiEnable verbose logging:
# .env
NODE_ENV=development
LOG_LEVEL=debugCheck logs:
# Application logs
npm run start:dev
# Docker service logs
docker compose logs -f postgres_db
docker compose logs -f redis-stack
# All logs
docker compose logs -f| Endpoint | Purpose |
|---|---|
GET / |
App health check |
GET /metrics |
Prometheus metrics |
- Check existing issues: GitHub Issues
- Read detailed docs: Check the
docs/folder - Open new issue: Include error logs and steps to reproduce
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit changes:
git commit -m 'Add amazing feature' - Push to branch:
git push origin feature/amazing-feature - Open a Pull Request
- Follow ESLint configuration
- Use Prettier for formatting
- Write tests for new features
- Update documentation as needed
This project is MIT licensed.
- NestJS - The progressive Node.js framework
- Prisma - Next-generation ORM
- PostgreSQL - The world's most advanced open source database
- Redis - In-memory data structure store
- BullMQ - Premium message queue for Node.js
- Prometheus - Monitoring system and time series database
- Grafana - Observability platform
- Loki - Log aggregation system
Made with β€οΈ by Pujon Das Auvi
β Star this repo β’ π Report Bug β’ β¨ Request Feature