A production-ready NestJS starter kit using Prisma ORM with PostgreSQL database. This boilerplate provides a clean architecture foundation for building scalable server-side applications.
- Overview
- Features
- Tech Stack
- Project Structure
- Prerequisites
- Installation
- Configuration
- Running the Application
- Database Management
- Testing
- Deployment
- Available Scripts
- Make Commands
- Conventions and AI Agent Rules
- License
Clean Nest Prisma PG is a starter template that combines NestJS framework with Prisma ORM and PostgreSQL. It follows clean architecture principles with a modular structure, separating concerns into reusable libraries for common utilities, repositories, and shared functionality.
- Authentication System: JWT-based authentication with access and refresh tokens, email verification, and password reset
- RBAC: Roles and permissions with
@PermissionAuth("entity:action")/@RoleAuth(...)decorators, resolved per request and cached in Redis - Database ORM: Prisma ORM with PostgreSQL adapter
- Soft Deletes:
deletedAton user rows — deletes stamp a timestamp, every read filters them out - Internationalization:
nestjs-i18nwithen/idcatalogs covering response messages, exception messages, validation messages, and email copy - Caching: Redis-based caching with cache-manager
- Queue System: BullMQ for background job processing
- Email Service: Nodemailer integration with Handlebars templating, queued via BullMQ
- Rate Limiting: Built-in throttler registered globally for API protection
- Validation: Class-validator and class-transformer for request validation, with translated field-level errors
- Code Quality: ESLint, Prettier, and Husky for code standards
- Docker Support: Docker Compose setup for PostgreSQL and Redis
- PM2 Deployment: Per-environment process definitions with one-command deploys
- Runtime: Bun
- Framework: NestJS 11
- Language: TypeScript
- ORM: Prisma 7
- Database: PostgreSQL 17
- Cache/Queue: Redis 8
- Authentication: Passport.js with JWT strategy
- Security: Fastify Helmet
- API Docs: Scalar (via
@scalar/nestjs-api-reference) - i18n: nestjs-i18n
- Env Validation: envalid
- Process Manager: PM2 (
ecosystem.config.js) - Testing: Jest
├── src/ # Application source code
│ ├── auth/ # Authentication module
│ ├── health/ # Health check endpoints
│ ├── settings/ # Users, roles, permissions (RBAC)
│ ├── app.module.ts # Root application module
│ ├── app.controller.ts # Root controller
│ └── main.ts # Application entry point
├── libs/ # Shared libraries (path-aliased)
│ ├── common/ # @common — guards, pipes, decorators, i18n, mail, cache, throttler, ResponseHandler
│ ├── config/ # @config — env validation, CORS, Helmet, Swagger
│ ├── repositories/ # @repositories — prisma singleton and repository factories
│ └── utils/ # @utils — hashing, JWT, dates, logging, constants
├── prisma/ # Prisma configuration
│ ├── migrations/ # Database migrations
│ ├── seed/ # Database seeders
│ └── schema.prisma # Prisma schema definition
├── test/ # End-to-end test configuration
├── .claude/ # Claude Code rules and slash commands
│ ├── rules/ # Path-scoped coding standards
│ └── commands/ # /commit, /update-todo, /audit-flow
├── docs/ # Generated documentation (e.g. audit-findings.md)
├── ecosystem.config.js # PM2 process definitions per environment
└── docker-compose.yml # Docker services configuration
Shared code lives in libs/ and is imported through the @common, @config, @repositories, and @utils aliases. Every public export must be re-exported from the lib's src/index.ts or the alias import will not resolve.
Before you begin, ensure you have the following installed:
- Bun (v1.0 or higher)
- PostgreSQL 17 (or use Docker)
- Redis 8 (or use Docker)
- Make (optional, for using Makefile commands)
- PM2 (only on deploy targets —
bun add -g pm2, or run the deploy commands withPM2='bunx pm2')
-
Clone the repository
git clone https://github.com/aolus-software/clean-nest-prisma-pg.git cd clean-nest-prisma-pg -
Install dependencies
bun install
-
Set up environment variables
cp .env.example .env- Start the database services (using Docker)
docker-compose up -d-
Run database migrations
make db-migrate-dev
Or directly:
bunx --bun prisma migrate dev bunx --bun prisma generate
-
Seed the database (optional)
bun run seed
Create a .env file in the root directory with the following variables:
# Application
APP_NAME="Clean Nest"
APP_VERSION=1.0.0
APP_SECRET=your_secret_key_here
APP_PORT=8001
APP_URL=localhost:8001
APP_TIMEZONE=UTC
NODE_ENV=development
# API Documentation (Scalar UI at /docs)
API_DOCS_ENABLED=true
# Frontend
FRONTEND_URL=http://localhost:3000
# Database
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/app_db?schema=public"
# JWT Authentication
JWT_SECRET=your_secret_key_here
JWT_REFRESH_SECRET=your_refresh_secret_key_here
JWT_EXPIRES_IN=1d
JWT_REFRESH_EXPIRES_IN=7d
# Rate Limiting
THROTTLER_TTL=60
THROTTLER_LIMIT=60
# CORS (comma-separated, defaults shown)
ALLOWED_ORIGINS="*"
ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"
ALLOWED_HEADERS="Content-Type, Authorization"
MAX_AGE=3600
CREDENTIALS=true
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_TTL=3600
# Mail Configuration
MAIL_HOST=
MAIL_PORT=
MAIL_SECURE=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM="noreply@example.com"
MAIL_DEFAULT_SUBJECT="Clean Nest"All environment variables are validated by envalid in libs/config/src/env/index.ts and read through getEnv(). Never read process.env directly — a variable that is not declared there is not available to the app. A missing or invalid required variable exits the process at boot rather than failing later.
NODE_ENV accepts development, dev, staging, production, or test. The dev and staging values exist for the deployed PM2 apps (see Deployment).
API_DOCS_ENABLED is the single switch for the /docs API reference and is independent of NODE_ENV — set it to true on any environment where the schema should be browsable, and leave it unset or false everywhere else. It defaults to false so an environment that never sets it cannot expose the schema by accident; .env.example turns it on for local development.
This template has three siblings — clean-nest-drizzle-pg, clean-nest-prisma-pg, clean-elysia,
and clean-elysia-prisma — and the two families use different names for the same concepts. The
names are internally consistent within each family and are deliberately left alone; this table exists
so an .env can be carried across without silently losing a setting.
14 variables are common to all four: APP_NAME, APP_PORT, APP_TIMEZONE, APP_URL,
DATABASE_URL, JWT_SECRET, MAIL_FROM, MAIL_HOST, MAIL_PORT, MAIL_SECURE, NODE_ENV,
REDIS_HOST, REDIS_PASSWORD, REDIS_PORT.
| Concern | NestJS family | Elysia family |
|---|---|---|
| App secret | APP_SECRET |
APP_KEY |
| CORS origin | ALLOWED_ORIGINS, ALLOWED_METHODS, ALLOWED_HEADERS, MAX_AGE, CREDENTIALS |
ALLOWED_HOST |
| Front-end URL | FRONTEND_URL |
CLIENT_URL |
| Mail credentials | MAIL_USERNAME, MAIL_PASSWORD, MAIL_DEFAULT_SUBJECT |
MAIL_USER, MAIL_PASS |
| Redis extra | REDIS_TTL |
REDIS_DB |
| JWT | JWT_SECRET, JWT_REFRESH_SECRET, JWT_EXPIRES_IN, JWT_REFRESH_EXPIRES_IN |
JWT_SECRET only |
NestJS-only (no Elysia equivalent): API_DOCS_ENABLED, THROTTLER_TTL, THROTTLER_LIMIT,
APP_VERSION.
Elysia-only: APP_CLUSTER_MODE, APP_CLUSTER_WORKERS, LOG_LEVEL, CLICKHOUSE_HOST,
CLICKHOUSE_USER, CLICKHOUSE_PASSWORD, CLICKHOUSE_DATABASE, and APP_REUSE_PORT
(clean-elysia only).
Two behaviours do not port, because the Elysia family has no equivalent variable:
API_DOCS_ENABLEDgates the docs UI here on an explicit flag defaulting tofalse, so an environment that never sets it cannot expose the schema. The Elysia family gates/docsonAPP_ENV !== "production"instead, publishing it on any non-production deployment.THROTTLER_TTL/THROTTLER_LIMITdrive the throttler from the environment here. The Elysia rate limit is hardcoded in its security plugin.
# Development mode (with hot-reload)
bun run start:dev
# Production mode
bun run build
bun run start:prod
# Debug mode
bun run start:debugThe application will be available at http://localhost:8001 (or the port specified in your .env file).
API documentation is available at http://localhost:8001/docs whenever API_DOCS_ENABLED=true. The variable defaults to false, so a deployment that does not set it serves no documentation route at all.
# Run migrations in development
bunx --bun prisma migrate dev
# Create a named migration from schema changes
bunx --bun prisma migrate dev --name <name>
# Run migrations in production
bunx --bun prisma migrate deploy
# Generate Prisma client
bunx --bun prisma generate
# Open Prisma Studio (database GUI)
bunx --bun prisma studio
# Reset database (caution: deletes all data)
bunx --bun prisma migrate reset --force
# Run database seeder
bun run seed
# Run specific seed file
bun run seed:file FILE=filenameAfter editing prisma/schema.prisma, run make db-migrate-dev to create and apply the migration and regenerate the typed client. Run make db-generate alone if you only need to refresh the client — a freshly cloned checkout needs it before bun run typecheck will pass.
The seeder creates the permission catalog as {user,role,permission}:{list,create,view,update,delete,restore}, plus the default roles and a superuser.
# Run unit tests
bun run test
# Run tests in watch mode
bun run test:watch
# Run tests with coverage
bun run test:cov
# Run end-to-end tests
bun run test:e2e
# Debug tests
bun run test:debugNote: the starter ships the Jest configuration but no test files yet —
bun run testcurrently matches zero specs. Unit tests are*.spec.tsalongside the code they cover; end-to-end tests usetest/jest-e2e.json.
Deployments are managed with PM2 using ecosystem.config.js, which defines one app per environment:
| App name | Mode | NODE_ENV | Memory restart |
|---|---|---|---|
clean-nest-prisma-pg-dev |
fork, 1 instance | dev |
2G |
clean-nest-prisma-pg-staging |
fork, 1 instance | staging |
2G |
clean-nest-prisma-pg-production |
cluster, max |
production |
4G |
Deploy with a single command per environment:
make deploy-dev
make deploy-staging
make deploy-productionEach target runs the full sequence — bun install --frozen-lockfile, prisma migrate deploy, prisma generate, bun run build — then reloads the PM2 app if it is already running (zero-downtime in cluster mode) or starts it from the ecosystem file if not, and finally pm2 save.
Managing running processes:
make pm2-status # pm2 list
make pm2-logs-dev # also pm2-logs-staging / pm2-logs-production
make pm2-stop-dev # also pm2-stop-staging / pm2-stop-productionNotes
pm2must be on yourPATH. If it is not installed globally, pass it in:make deploy-dev PM2='bunx pm2'.- Per-environment configuration comes from the
.envfile in the deploy directory (env_file: ".env"). The ecosystem file sets onlyNODE_ENV. - Adding a new environment means adding it to both
ecosystem.config.jsand theNODE_ENVchoices inlibs/config/src/env/index.ts— envalid rejects an unknown value and the process exits at boot. - Logs are written to
logs/<env>-out.logandlogs/<env>-error.log. The directory is created by the deploy target and is gitignored. - To rename the apps, change
PM2_APP_PREFIXin theMakefileand thenamefields inecosystem.config.jstogether. - Cluster mode is safe because nothing here runs on a timer. If you add scheduled work, it will run once per instance unless you guard it with a lock.
| Script | Description |
|---|---|
bun run build |
Build the application |
bun run start |
Start the application |
bun run start:dev |
Start in development mode with hot-reload |
bun run start:debug |
Start in debug mode |
bun run start:prod |
Start in production mode |
bun run lint |
Lint and fix code |
bun run format |
Format code with Prettier |
bun run typecheck |
Run TypeScript type checks |
bun run test |
Run unit tests |
bun run test:e2e |
Run end-to-end tests |
bun run test:cov |
Run tests with coverage |
bun run seed |
Run database seeders |
For convenience, a Makefile is provided with shortcut commands:
| Command | Description |
|---|---|
make help |
Display available commands |
make dev |
Start development server |
make start |
Start the project |
make typecheck |
Run type checks |
make build |
Build the project |
make lint |
Lint the project |
make format |
Format the project |
make test |
Run tests |
make test-watch |
Run tests in watch mode |
make db-generate |
Generate the Prisma client |
make db-format |
Format schema.prisma (run after editing it) |
make db-migrate |
Run database migrations (production) |
make db-migrate-dev |
Run database migrations (development) + generate |
make db-seed |
Run database seeder |
make db-reset |
Reset the database |
make db-studio |
Start Prisma Studio |
make deploy-prep |
Prepare for deployment (install, migrate, build) |
make deploy-dev |
Deploy and start/reload the dev PM2 app |
make deploy-staging |
Deploy and start/reload the staging PM2 app |
make deploy-production |
Deploy and start/reload the production PM2 app |
make pm2-status |
List PM2 processes |
make pm2-logs-<env> |
Tail logs for dev, staging, or production |
make pm2-stop-<env> |
Stop dev, staging, or production |
To install Make on Ubuntu:
sudo apt update
sudo apt install makeArchitecture notes and coding standards live alongside the code so both humans and AI coding agents work from the same source of truth:
CLAUDE.md— project overview, commands, architecture, and the non-obvious behaviours worth knowing before making a change..claude/rules/— path-scoped standards applied per file type:controller.md,service.md,repository.md,dto.md,module.md,schema.md,i18n.md,response-codes.md,routes.md,rate-limiting.md,clean-code.md,shared-code.md, and more..claude/commands/—/commit(Conventional Commit workflow),/update-todo, and/audit-flow(read-only whole-codebase audit that writes explained findings todocs/audit-findings.mdand never modifies code).
Core conventions at a glance:
- Request flow is Controller → Service → Repository. Controllers handle HTTP only, services own business logic and transactions, repositories run Prisma queries.
- Repositories are factory functions, not classes:
UserRepository().findOne(id). Pass a transaction client to the factory to join a transaction:UserRepository(tx).findOne(id). - Responses go through
ResponseHandlerand are sent viares.status(code).send(...). - Permission strings are
entity:actionwith a singular entity (user:create). - Use
PATCH, notPUT, for updates. - Style is tabs, double quotes, semicolons; no
anyexceptcatch (err: unknown).
This project is licensed under the MIT License.