A production-ready Express boilerplate using Node.js 22 (ESM) with:
- Winston logger (colorized, padded output; HTTP request logger)
- Prisma (PostgreSQL) with dynamic DB opt-in (app runs without DATABASE_URL)
- JWT auth (access token) + role-based protection hook
- Zod request validation middleware
- Security middlewares:
helmet,hpp,cors,express-rate-limit - ESLint (flat) + Prettier (recommended configs)
- Docker & Docker Compose for prod and dev (nodemon)
- .env layering:
.env+.env.{NODE_ENV}viadotenv - Seed: default roles/permissions + admin user (
administrator / admin@example.com)
- Project Structure
- Features
- Getting Started
- Environment Variables
- Scripts
- Docker Usage
- Database (Prisma)
- Auth
- Validation
- Security
- Logging
- Request Testing
- Troubleshooting
├─ prisma/
│ ├─ schema.prisma
│ └─ seed.js
├─ src/
│ ├─ config/
│ │ ├─ prisma.js
│ │ ├─ logger.js
│ │ └─ jwt.js
│ ├─ controllers/
│ │ └─ auth-controller.js
│ ├─ middleware/
│ │ ├─ authenticate.js
│ │ ├─ authorize.js
│ │ ├─ request-logger.js
│ │ └─ validate.js
│ ├─ routes/
│ │ ├─ auth-routes.js
│ │ └─ index.js
│ └─ index.js
├─ entrypoint.sh
├─ Dockerfile
├─ Dockerfile.dev
├─ docker-compose.yml # production
├─ docker-compose.dev.yml # development (nodemon + local PG)
├─ .env.example
├─ eslint.config.js
├─ .prettierrc.json
├─ .prettierignore
├─ .dockerignore
├─ .gitignore
└─ README.md
- Node.js v22, ESM (
"type": "module"). - All source code in
src/.
- ESLint flat config with
eslint:recommendedand compatibility for Prettier. - Prettier for consistent formatting.
- Winston logger with padded uppercase levels and timestamp.
- HTTP request logger middleware (method, URL, status, duration).
dotenvloads.envand.env.{NODE_ENV}(e.g.,.env.development).- App still runs without DATABASE_URL (DB features disabled).
- Prisma with PostgreSQL.
- Dynamic client: connect only when
DATABASE_URLexists. - Seed default: Roles (
Administrator,Manager,Team Leader), permissions, admin user.
helmetfor secure headers;app.disable('x-powered-by').hppto mitigate HTTP Parameter Pollution.corsconfigurable origins/credentials.express-rate-limitfor basic rate limiting.
- JWT (access token) with
authenticatemiddleware. authorize('RoleName')helper for RBAC.
- Zod schema +
validate(schema)middleware. - Clean error shape:
{ message, errors: [{ field, message }] }.
- Prod image: multi-stage,
npm ci,prisma migrate deploy, optional seed. - Dev compose: nodemon, local Postgres service, volume-mounted code.
- Install deps
npm install- Copy envs
cp .env.example .env.development
cp .env.example .env- Local dev
npm run dev- Prisma (if using DB)
npx prisma migrate dev --name init
npx prisma db seedApp runs at: http://localhost:${PORT || 3000}
.env.example (sample):
# App
NODE_ENV=development
PORT=3000
LOG_LEVEL=info
# JWT
JWT_SECRET=super-secret-key
JWT_EXPIRES_IN=1h
# Database (optional; if missing, app runs without DB)
# For docker-compose dev PG, use: postgres://user:password@db:5432/mydb?schema=public
DATABASE_URL=postgresql://user:password@localhost:5432/mydb?schema=public
# CORS
CORS_ORIGIN=http://localhost:5173
CORS_CREDENTIALS=true
# Rate limiter
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100docker-compose up --build- Uses
Dockerfile, runsentrypoint.sh:npm ci --only=productionprisma generateprisma migrate deploy- optional seed: set
RUN_SEED=true npm run start
External DB: set DATABASE_URL to remote Postgres and remove/ignore db service if not needed.
docker-compose -f docker-compose.dev.yml up --build- Uses
Dockerfile.dev - Nodemon + code mounted
- Local Postgres service (
db), seeds enabled by default if configured
- Dynamic client:
src/config/prisma.jsonly connects ifprocess.env.DATABASE_URLis present. - Seed (
prisma/seed.js):- Roles:
Administrator,Manager,Team Leader - Admin user:
administrator / admin@example.com / admin123(bcrypt hashed) - Permissions examples using
CaslAction(e.g.,MANAGE all)
- Roles:
Composite unique for permissions (recommended):
model CaslPermission {
id Int @id @default(autoincrement())
action CaslAction
subject String @db.VarChar(255)
conditions Json?
inverted Boolean @default(false)
roleId Int
role CaslRole @relation(fields: [roleId], references: [id])
createdAt DateTime @default(now())
@@unique([roleId, action, subject])
}Migrations
- Dev:
npx prisma migrate dev --name <desc> - Prod:
npx prisma migrate deploy
Seeding
npx prisma db seedLogin: POST /api/auth/login
{
"usernameOrEmail": "administrator",
"password": "admin123"
}Middleware
authenticate→ verifies JWT, setsreq.userauthorize('Administrator')→ checks role
Protected example
router.get('/admin', authenticate, authorize('Administrator'), handler);Zod schema (example loginSchema):
const loginSchema = z.object({
usernameOrEmail: z.string().min(1, 'Username/email is required'),
password: z.string().min(6, 'Password must be at least 6 characters')
});Middleware validate(schema) returns:
{
"message": "Validation failed",
"errors": [{ "field": "password", "message": "..." }]
}Robust handling for versions where ZodError.message is JSON: it parses when needed.
helmet()→ secure headersapp.disable('x-powered-by')hpp()→ prevent HTTP Parameter Pollutioncors({ origin: CORS_ORIGIN, credentials: true })rateLimit({ windowMs: RATE_LIMIT_WINDOW_MS, max: RATE_LIMIT_MAX })- Passwords hashed via
bcrypt - JWT secrets kept in env (never committed)
Winston logger with:
- Timestamp:
[YYYY-MM-DD HH:mm:ss] - Uppercased, padded levels (INFO, WARN, ERROR, DEBUG)
- HTTP request logger output example:
[2025-08-03 16:23:17] HTTP : GET / 304 5.7ms
VS Code – REST Client (.http):
### Login
POST http://localhost:3000/api/auth/login
Content-Type: application/json
{
"usernameOrEmail": "administrator",
"password": "admin123"
}Thunder Client (extension) also supported.
-
@prisma/client did not initialize yet
Ensure you didn’t set a customgenerator.output. Runnpx prisma generate. -
Zod errors show as string
Middleware handles botherror.errorsand parsederror.messagefallback. -
Cannot destructure ... req.body undefined
Addapp.use(express.json())before routes and sendContent-Type: application/json. -
Docker cannot connect to DB
CheckDATABASE_URLhost (dbservice name in compose), credentials, and exposed port. -
Seeding duplicates
Seed usesupsertwith unique constraints (e.g., role name, permission tuple).
MIT © 2025 Ravhi Rizaldi See the LICENSE file for details.
{ "dev": "nodemon --env-file=.env.development src/index.js", "start": "node --env-file=.env src/index.js", "lint": "eslint .", "lint:fix": "eslint . --fix", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:deploy": "prisma migrate deploy", "prisma:seed": "node --env-file=.env prisma/seed.js" }