Skip to content

DiamondJdev/NestJSTemplate

Repository files navigation

πŸš€ NestJS Template

A production-ready, opinionated NestJS backend template with JWT authentication, role-based access control, structured logging, and SQLite persistence β€” ready to clone and extend.

NestJS TypeScript Node.js SQLite Jest License: MIT

ESLint bcrypt TypeORM Socket.io


πŸ“‹ Table of Contents


✨ Features

Feature Description
πŸ” JWT Auth Access + refresh token rotation with bcrypt-hashed token storage
πŸ›‘οΈ RBAC Decorator-driven role-based access control (admin, user)
πŸ—„οΈ TypeORM + SQLite Zero-config local persistence via SQLite; swap to Postgres/MySQL easily
βœ… Validation class-validator with strict password policy enforcement
πŸ“ Structured Logging File-based multi-channel logs (combined, error, stats) + HTTP interceptor
❀️ Health Checks /health endpoint with live database connectivity probe
πŸ”Œ WebSocket Ready socket.io and @nestjs/websockets pre-installed and configured
πŸ§ͺ Testing Jest unit tests with mocked repositories; coverage reports included
πŸ”’ Timing-Attack Prevention Constant-time hash comparison even for non-existent users

πŸ—οΈ Tech Stack

Layer Technology
Framework NestJS 11
Language TypeScript 5.7
ORM TypeORM 0.3
Database SQLite (swappable)
Auth @nestjs/jwt + bcrypt
Validation class-validator + class-transformer
WebSockets Socket.io 4
Testing Jest 30 + Supertest
Linting ESLint 9 (flat config) + typescript-eslint

🎯 Opinionated Decisions

This template reflects specific architectural choices made to promote security, maintainability, and developer experience. Understanding these decisions will help you extend the template confidently.

πŸ” Refresh Token Hashing

Refresh tokens are never stored in plaintext. After generation, the raw token is hashed with bcrypt (cost factor 10) before persisting to the database. Only the hash is stored in user.refreshTokenHash.

Why? If your database is ever compromised, attackers cannot reuse refresh tokens. The raw token lives only in the HTTP response.

User logs in β†’ raw tokens generated β†’ refreshToken returned to client
                                     β†’ refreshTokenHash saved to DB

⏱️ Timing Attack Prevention in Login

When a user provides an unknown username, the service still performs a bcrypt.compare() call against a dummy hash. This ensures login attempts for real and fake users take the same amount of time.

// auth.service.ts
const comparisonHash = user ? user.password : '$2b$12$invalidhashinvalidhas$2b$12$invalidhashinvalidhas';
const isMatch = await bcrypt.compare(loginUserDto.password, comparisonHash);

Why? Without this, an attacker could measure response time to enumerate valid usernames.

πŸ”„ Token Rotation on Every Refresh

Every call to PATCH /auth/refresh issues a brand new access + refresh token pair and invalidates the previous refresh token hash in the database. Single-use refresh tokens prevent token replay attacks.

🧹 Whitelist Validation (DTO Stripping)

ValidationPipe is configured globally with whitelist: true. Any property sent in a request body that is not declared in a DTO is silently stripped before it reaches your controller.

// main.ts
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,       // Strip unknown properties
  transform: true,       // Auto-transform primitive types
  stopAtFirstError: true // Return first error only
}));

πŸ”‘ Strong Password Policy

Registration enforces a strict password policy via class-validator's @IsStrongPassword:

  • Minimum 12 characters
  • At least 1 uppercase letter
  • At least 1 lowercase letter
  • At least 1 number
  • At least 1 symbol
  • Maximum 128 characters (prevents bcrypt DoS)

πŸ†” UUID Primary Keys

All user records use auto-generated UUID v4 primary keys instead of sequential integers. This prevents ID enumeration attacks and simplifies horizontal scaling.

πŸ“ File-Based Logging (No External Dependency)

Logging writes to three files in the logs/ directory with zero external dependencies (no Elasticsearch, no Datadog). This keeps the template self-contained while providing enough structure to pipe into any logging platform.

🌐 CORS (Development Default)

CORS is currently open to all origins (origin: true). This is intentional for a development template. Before deploying to production, restrict this to your trusted origins:

// main.ts β€” update before production
app.enableCors({
  origin: ['https://your-frontend.com'],
  credentials: true,
});

πŸ“ Architecture

Module Structure

src/
β”œβ”€β”€ main.ts                          # Bootstrap: logger, pipes, CORS, port
└── modules/
    β”œβ”€β”€ app.module.ts                # Root module (imports all feature modules)
    β”œβ”€β”€ auth/                        # Authentication (login, register, refresh)
    β”‚   β”œβ”€β”€ auth.controller.ts
    β”‚   β”œβ”€β”€ auth.service.ts
    β”‚   β”œβ”€β”€ auth.module.ts
    β”‚   β”œβ”€β”€ guard/
    β”‚   β”‚   β”œβ”€β”€ jwt-auth.guard.ts    # Validates Bearer tokens on protected routes
    β”‚   β”‚   └── body-required.guard.ts
    β”‚   └── dto/
    β”‚       β”œβ”€β”€ CreateUser.dto.ts    # Strict password validation rules
    β”‚       β”œβ”€β”€ loginUser.dto.ts
    β”‚       └── refreshUserTokens.dto.ts
    β”œβ”€β”€ users/                       # User management (CRUD, role-protected)
    β”‚   β”œβ”€β”€ users.controller.ts
    β”‚   └── users.module.ts
    β”œβ”€β”€ roles/                       # Role management module
    β”‚   β”œβ”€β”€ roles.controller.ts
    β”‚   β”œβ”€β”€ roles.service.ts
    β”‚   └── roles.module.ts
    β”œβ”€β”€ db/                          # Database abstraction (TypeORM operations)
    β”‚   β”œβ”€β”€ db.service.ts
    β”‚   β”œβ”€β”€ db.module.ts
    β”‚   β”œβ”€β”€ db.service.spec.ts
    β”‚   └── utils/
    β”‚       └── userConversion.ts
    β”œβ”€β”€ jwt/                         # JWT token generation & validation
    β”‚   β”œβ”€β”€ jwt.service.ts
    β”‚   └── jwt.module.ts
    └── common/                      # Shared utilities, guards, entities, logging
        β”œβ”€β”€ entities/
        β”‚   └── user.entity.ts       # TypeORM User entity (UUID, unique username)
        β”œβ”€β”€ flow/
        β”‚   β”œβ”€β”€ roles.guard.ts       # RBAC guard (checks @Roles() decorator)
        β”‚   └── roles.decorator.ts   # @Roles('admin') decorator
        β”œβ”€β”€ health/
        β”‚   └── controller/
        β”‚       └── health.controller.ts
        β”œβ”€β”€ logging/
        β”‚   β”œβ”€β”€ services/
        β”‚   β”‚   └── logger.service.ts
        β”‚   └── interceptors/
        β”‚       └── logging.interceptor.ts
        └── utils/
            β”œβ”€β”€ roleChecker.ts
            └── userRole.enum.ts

Data Model

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  User                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ id           β”‚ UUID (PK, auto-generated) β”‚
β”‚ username     β”‚ VARCHAR(64) UNIQUE        β”‚
β”‚ password     β”‚ VARCHAR (bcrypt hash)     β”‚
β”‚ role         β”‚ VARCHAR (default: 'user') β”‚
β”‚ refreshTokenHash β”‚ VARCHAR (bcrypt hash, nullable) β”‚
β”‚ createdAt        β”‚ TIMESTAMP (auto-set)            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Request Lifecycle

HTTP Request
     β”‚
     β–Ό
LoggingInterceptor     ← logs method, url, duration, userId
     β”‚
     β–Ό
JwtAuthGuard           ← validates Bearer token (protected routes)
     β”‚
     β–Ό
RolesGuard             ← checks @Roles() decorator (admin routes)
     β”‚
     β–Ό
ValidationPipe         ← strips unknown props, validates DTO
     β”‚
     β–Ό
Controller β†’ Service β†’ DbService β†’ SQLite
     β”‚
     β–Ό
HTTP Response (+ logged by interceptor)

⚑ Quickstart

Prerequisites

  • Node.js β‰₯ 18 (download)
  • npm β‰₯ 9

1. Clone & Install

git clone https://github.com/DiamondJdev/NestJSTemplate.git my-app
cd my-app
npm install

2. Configure Environment

cp .env.example .env

Edit .env with your secrets (see Environment Variables below).

Note: A .env.example file is provided as a template. The app will start on port 5200 by default if PORT is not set.

3. Start the Development Server

npm run start:dev

The dev server starts at http://localhost:8080 (the --port 8080 flag in start:dev overrides the default). In production (npm start / npm run start:prod), the server listens on the port defined by PORT in your .env file, which defaults to 5200.

4. Verify It's Working

curl http://localhost:8080/
# β†’ {"message":"OK","version":"1.0.0","database":{"status":"connected","latency":12},"backend":{"status":"running","uptime":123456},"timestamp":"2025-01-01T00:00:00.000Z","mode":"development"}

5. Register Your First User

curl -X POST http://localhost:8080/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "adminuser",
    "password": "MyStr0ng!Password"
  }'

Example Response:

{
  "message": "User registered successfully",
  "userID": "550e8400-e29b-41d4-a716-446655440000",
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

6. Access a Protected Route

curl http://localhost:8080/auth/me \
  -H "Authorization: Bearer <your-access-token>"

🐳 Running with Docker

Prerequisites: Docker Desktop (or Docker Engine + Compose v2).

  1. Copy the env template and fill in secrets:

    cp .env.example .env

    The stack reads ALLOWED_ORIGINS, JWT_SECRET, TOKEN_ENCRYPTION_KEY, and the UPSTASH_* keys from .env; without them the API crashes on boot. Compose overrides DATABASE_URL/PORT so the container always uses the bundled Postgres on port 5200.

  2. Start the dev stack (API + Postgres):

    docker compose up --build

    Editing files under src/ hot-reloads the running container. Note: the file watcher detects the change and triggers a rebuild, but the restarted process can fail to bind port 5200 if the previous process hasn't released it yet (no graceful shutdown handling in this template) β€” the old process then keeps serving stale code. If your changes don't seem to take effect, run docker compose restart api.

  3. Stop the stack (Postgres data persists in the postgres_data volume):

    docker compose down

Production image

Build the lean production image from the multi-stage Dockerfile's prod target:

docker build --target prod -t template-backend:prod .

It compiles to dist/, installs production-only dependencies, and runs node dist/src/main. Supply DATABASE_URL, ALLOWED_ORIGINS, and other env vars at runtime via your orchestrator.


πŸ” Environment Variables

Create a .env file in the project root. The application reads all config via @nestjs/config.

Variable Required Default Description
PORT No 5200 HTTP server port
JWT_SECRET Yes β€” Secret key for signing access tokens
JWT_EXPIRATION No 15m Access token TTL (e.g., 15m, 1h)
JWT_REFRESH_EXP No 7d Refresh token TTL (e.g., 7d, 30d)
NODE_ENV No development Set to production to suppress console logs

.env.example:

PORT=5200

# Generate a strong secret: openssl rand -hex 64
JWT_SECRET=your-super-secret-key-change-me-in-production
JWT_EXPIRATION=15m
JWT_REFRESH_EXP=7d

NODE_ENV=development

⚠️ Never commit your .env file. It is already included in .gitignore.


πŸ“‘ API Reference

Base URL (development – npm run start:dev): http://localhost:8080

For production or when running npm start, the base URL is http://localhost:5200 (or http://localhost:${PORT} if you change the PORT value).

Authentication

Method Endpoint Auth Body Description
POST /auth/register None { username, password } Create a new account
POST /auth/login None { username, password } Authenticate and receive tokens
PATCH /auth/refresh Bearer (access) { refreshToken } Rotate access + refresh tokens
GET /auth/me Bearer (access) β€” Check if the current token is valid

Users

Method Endpoint Auth Roles Description
GET /users Bearer admin List all users
GET /users/me Bearer Any Get current user's details
PATCH /users/:uuid Bearer Any* Update user by UUID
DELETE /users/:uuid Bearer admin Delete user by UUID

*Users can only update their own profile; admins can update any profile.

Health

Method Endpoint Auth Description
GET / None Returns server status and DB connectivity

Health response example:

{
  "status": "ok",
  "database": {
    "status": "connected",
    "latencyMs": 12
  },
  "backend": {
    "uptimeSeconds": 12345,
    "version": "1.0.0",
    "mode": "production"
  },
  "timestamp": "2025-01-01T12:00:00.000Z"
}

Request / Response Examples

POST /auth/login

Request:

{
  "username": "myuser",
  "password": "MyStr0ng!Password"
}

Success (200):

{
  "message": "User logged in successfully",
  "userID": "550e8400-e29b-41d4-a716-446655440000",
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Failure (401):

{
  "statusCode": 401,
  "message": "Invalid Username or Password"
}
PATCH /auth/refresh

Request (with Authorization: Bearer <accessToken> header):

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Success (200):

{
  "message": "Token refreshed successfully",
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

πŸ”‘ Authentication Flow

This template uses a dual-token authentication strategy:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Client  β”‚         β”‚   NestJS API β”‚         β”‚    Database    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚                      β”‚                          β”‚
     β”‚   POST /auth/login    β”‚                          β”‚
     │──────────────────────▢│                          β”‚
     β”‚                      β”‚   findOne(username)       β”‚
     β”‚                      │─────────────────────────▢│
     β”‚                      │◀─────────────────────────│
     β”‚                      β”‚   bcrypt.compare()        β”‚
     β”‚                      β”‚   rotateTokens()          β”‚
     β”‚                      β”‚   saveRefreshToken(hash)  β”‚
     β”‚                      │─────────────────────────▢│
     β”‚ { accessToken,        β”‚                          β”‚
     β”‚   refreshToken }      β”‚                          β”‚
     │◀──────────────────────│                          β”‚
     β”‚                      β”‚                          β”‚
     β”‚  (use accessToken     β”‚                          β”‚
     β”‚   for API calls)      β”‚                          β”‚
     β”‚                      β”‚                          β”‚
     β”‚  PATCH /auth/refresh  β”‚                          β”‚
     β”‚  Bearer: accessToken  β”‚                          β”‚
     β”‚  body: refreshToken   β”‚                          β”‚
     │──────────────────────▢│                          β”‚
     β”‚                      β”‚  compare(token, hash)    β”‚
     β”‚                      │─────────────────────────▢│
     β”‚                      β”‚   rotateTokens()          β”‚
     β”‚                      β”‚   saveNewHash()           β”‚
     β”‚                      │─────────────────────────▢│
     β”‚ { new accessToken,    β”‚                          β”‚
     β”‚   new refreshToken }  β”‚                          β”‚
     │◀──────────────────────│                          β”‚

Token Details:

Token Algorithm Default Expiry Storage
Access Token HS256 15 minutes Client memory / header
Refresh Token HS256 7 days Client (secure cookie / local)
Refresh Token Hash bcrypt (cost 10) β€” Database (refreshTokenHash column)

πŸ›‘οΈ Role-Based Access Control

The template ships with two roles defined in userRole.enum.ts:

enum UserRole {
  ADMIN = 'admin',
  USER  = 'user',
}

Roles are enforced using a custom decorator + guard combination:

// Apply to any controller or route handler
@Roles('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Get()
findAll() { ... }

How it works:

  1. JwtAuthGuard validates the Bearer token and attaches req.user (with id and role).
  2. RolesGuard reads the @Roles() metadata and compares it against req.user.role.
  3. If the role does not match, a 403 Forbidden response is returned.

Default Role Assignment:

All newly registered users are assigned the user role. Admin accounts must be promoted manually (via direct DB update or a dedicated admin endpoint that you add to your project).


πŸ“ Logging

The template includes a zero-dependency, file-based logging system.

Log Files

All files are written to the logs/ directory (auto-created, excluded from git):

File Contents
logs/combined.log All log levels
logs/error.log Errors and warnings only
logs/stats.log User activity events (login, registration, etc.)

Log Format

2026-02-11T17:50:00.123Z [INFO][Bootstrap] Server running on http://0.0.0.0:5200
2026-02-11T17:50:01.456Z [INFO][HTTP] GET /users 200 {"duration":"12ms","userId":"abc-123"}
2026-02-11T17:50:02.789Z [ERROR][AuthService] Invalid credentials {"trace":"Error: ..."}
2026-02-11T17:50:03.012Z [INFO][UserStats] User abc-123 - login {"ip":"127.0.0.1"}

HTTP Request Logging

All incoming requests are automatically logged by LoggingInterceptor (applied globally). No manual configuration is needed. Each log entry includes:

  • HTTP method and URL
  • Response status code
  • Response time (ms)
  • User ID (if authenticated)

Using the Logger in Your Code

// Adjust the import path based on your file's location or configured path aliases
import { LoggerService } from 'src/modules/common/logging/services/logger.service';

@Injectable()
export class MyService {
  constructor(private readonly logger: LoggerService) {}

  doSomething() {
    this.logger.log('Info message', 'MyService');
    this.logger.warn('Warning message', 'MyService');
    this.logger.error('Error message', error.stack, 'MyService');

    // Log user actions to stats.log
    this.logger.logUserStats('purchase', userId, { amount: 9.99 });
  }
}

See LOGGING.md for the full logging documentation.


πŸ§ͺ Testing

# Run all unit tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage report
npm run test:cov

# Run end-to-end tests
npm run test:e2e

Test Structure

Tests live alongside the source files (*.spec.ts):

src/modules/db/db.service.spec.ts              # DbService unit tests
src/modules/common/health/controller/
    health.controller.spec.ts                  # HealthController unit tests

Tests use Jest with @nestjs/testing and mocked TypeORM repositories β€” no real database is required to run the test suite.

Example test approach:

// Repositories are mocked with jest.fn() factories
const mockRepository = {
  findOne: jest.fn(),
  save: jest.fn(),
  delete: jest.fn(),
};

const module = await Test.createTestingModule({
  providers: [
    DbService,
    { provide: getRepositoryToken(User), useValue: mockRepository },
  ],
}).compile();

πŸ”„ Replacing SQLite with PostgreSQL

SQLite is used for quick local setup. To swap to PostgreSQL:

  1. Install the driver: npm install pg
  2. Update the TypeORM config in app.module.ts:
TypeOrmModule.forRoot({
  type: 'postgres',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT ?? '5432'),
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  autoLoadEntities: true,
  synchronize: process.env.NODE_ENV !== 'production', // use migrations in prod
})

πŸ”Œ WebSocket Support

Socket.io and @nestjs/websockets are pre-installed. To add a WebSocket gateway:

nest generate gateway events

This will scaffold a gateway class in src/events/ that you can extend with your real-time logic.


🀝 Contributing

Contributions are welcome! Please follow these steps:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Make your changes and add tests where appropriate
  4. Ensure all tests pass: npm test
  5. Lint your code: npm run lint
  6. Open a Pull Request describing your changes

πŸ“„ License

This project is licensed under the MIT License. See the LICENSE file for details.


Made with ❀️ by Cameron Ginther, Chloe Hunter, and Brett Berry

About

An opinionated NestJS template that serves as a starter for all of my backends. Complete with JWT Auth, User Management, TypeORM integrations, and more

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages