Skip to content

ravhirizaldi/express-boilerplate

Repository files navigation

Node.js Express Boilerplate

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} via dotenv
  • Seed: default roles/permissions + admin user (administrator / admin@example.com)

Table of Contents


Project Structure

├─ 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

Features

Runtime & Language

  • Node.js v22, ESM ("type": "module").
  • All source code in src/.

Lint & Format

  • ESLint flat config with eslint:recommended and compatibility for Prettier.
  • Prettier for consistent formatting.

Logger

  • Winston logger with padded uppercase levels and timestamp.
  • HTTP request logger middleware (method, URL, status, duration).

Env & Config

  • dotenv loads .env and .env.{NODE_ENV} (e.g., .env.development).
  • App still runs without DATABASE_URL (DB features disabled).

Database

  • Prisma with PostgreSQL.
  • Dynamic client: connect only when DATABASE_URL exists.
  • Seed default: Roles (Administrator, Manager, Team Leader), permissions, admin user.

Security

  • helmet for secure headers; app.disable('x-powered-by').
  • hpp to mitigate HTTP Parameter Pollution.
  • cors configurable origins/credentials.
  • express-rate-limit for basic rate limiting.

Auth

  • JWT (access token) with authenticate middleware.
  • authorize('RoleName') helper for RBAC.

Validation

  • Zod schema + validate(schema) middleware.
  • Clean error shape: { message, errors: [{ field, message }] }.

Docker

  • Prod image: multi-stage, npm ci, prisma migrate deploy, optional seed.
  • Dev compose: nodemon, local Postgres service, volume-mounted code.

Getting Started

  1. Install deps
npm install
  1. Copy envs
cp .env.example .env.development
cp .env.example .env
  1. Local dev
npm run dev
  1. Prisma (if using DB)
npx prisma migrate dev --name init
npx prisma db seed

App runs at: http://localhost:${PORT || 3000}


Environment Variables

.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=100

Scripts

{
  "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"
}

Docker Usage

Production

docker-compose up --build
  • Uses Dockerfile, runs entrypoint.sh:
    • npm ci --only=production
    • prisma generate
    • prisma 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.

Development

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

Database (Prisma)

  • Dynamic client: src/config/prisma.js only connects if process.env.DATABASE_URL is 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)

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 seed

Auth

Login: POST /api/auth/login

{
  "usernameOrEmail": "administrator",
  "password": "admin123"
}

Middleware

  • authenticate → verifies JWT, sets req.user
  • authorize('Administrator') → checks role

Protected example

router.get('/admin', authenticate, authorize('Administrator'), handler);

Validation

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.


Security

  • helmet() → secure headers
  • app.disable('x-powered-by')
  • hpp() → prevent HTTP Parameter Pollution
  • cors({ 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)

Logging

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

Request Testing

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.


Troubleshooting

  • @prisma/client did not initialize yet
    Ensure you didn’t set a custom generator.output. Run npx prisma generate.

  • Zod errors show as string
    Middleware handles both error.errors and parsed error.message fallback.

  • Cannot destructure ... req.body undefined
    Add app.use(express.json()) before routes and send Content-Type: application/json.

  • Docker cannot connect to DB
    Check DATABASE_URL host (db service name in compose), credentials, and exposed port.

  • Seeding duplicates
    Seed uses upsert with unique constraints (e.g., role name, permission tuple).


License

MIT © 2025 Ravhi Rizaldi See the LICENSE file for details.

About

A production-ready Express boilerplate using Node.js 22

Topics

Resources

License

Stars

Watchers

Forks

Contributors