Production‑ready authentication template — NestJS 11 · Prisma ORM · JWT · RBAC · Rate‑limiting · Structured logging
A standardized REST API for user authentication, built as a starter template that scales from local prototyping to production deployments.
| Feature | Status |
|---|---|
| Registration via single‑use invite codes | ✅ |
| Invite code generation (Admin / Moderator) | ✅ |
| JWT access tokens (short‑lived) | ✅ |
| Refresh token rotation (UUID + DB storage) | ✅ |
Atomic rotation (concurrent‑race safe via updateMany) |
✅ |
| Role‑Based Access Control (Admin / Moderator / Donator / User) | ✅ |
| Rate limiting (configurable + stricter per‑route for auth) | ✅ |
| Structured JSON logging (Pino) | ✅ |
Request correlation ID (genReqId + x-request-id) |
✅ |
| Security headers (Helmet) | ✅ |
| Health check endpoint (DB ping) | ✅ |
| Prometheus metrics endpoint | ✅ |
| Email / username case normalization (lowercased on register/login) | ✅ |
| Activity logging — server‑side activity feed per user | ✅ |
| Dashboard stats endpoint (total users, new‑7d, role distribution) | ✅ |
| Profile update (email change) | ✅ |
| Password change | ✅ |
| User self‑deletion | ✅ |
| Admin user deletion | ✅ |
| Admin user list pagination | ✅ |
DTO‑validated role updates (@IsEnum) |
✅ |
| Scheduled expired‑token cleanup (hourly) | ✅ |
| Production‑env validation at startup | ✅ |
| Multi‑database support (5 providers) | ✅ |
| Swagger / OpenAPI docs | ✅ |
| Docker Compose (NestJS + PostgreSQL) | ✅ |
| E2E test suite (41 tests) | ✅ |
npm install
npm run setup # generate Prisma client → push schema → seed
npm run dev # http://localhost:4000# register
curl -X POST http://localhost:4000/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"username":"alice","email":"alice@test.com","password":"Str0ng!Pass","inviteCode":"INVITE-2024-001"}'
# login
curl -X POST http://localhost:4000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"alice","password":"Str0ng!Pass"}'
# profile
curl http://localhost:4000/api/user/profile \
-H 'Authorization: Bearer <TOKEN>'Swagger UI is available at
http://localhost:4000/docs.
The API is fully documented with @nestjs/swagger (OpenAPI 3.0). After starting the server, visit:
| Resource | URL |
|---|---|
| Swagger UI | http://localhost:4000/docs |
| OpenAPI JSON | http://localhost:4000/docs-json |
Endpoints are organized under two tags in the Swagger UI:
| Tag | Endpoints | Auth |
|---|---|---|
| Authentication | POST /auth/register · POST /auth/login · POST /auth/refresh · POST /auth/logout · POST /auth/invite-codes |
Register/login/refresh/logout are @Public() — no token needed; invite-codes requires ADMIN or MODERATOR role + Bearer token |
| Users | GET /user/profile · PATCH /user/profile · GET /user/stats · GET /user · PATCH /user/:id/role · PATCH /user/profile/password · DELETE /user/profile · DELETE /user/:id |
Bearer token required; list, role, and delete endpoints restricted to ADMIN |
| Activity | POST /activity · GET /activity |
Bearer token required |
The Swagger UI has an Authorize button in the top‑right corner. Paste your JWT access token there (without the Bearer prefix) to unlock protected endpoints. Tokens can be obtained from POST /auth/login or POST /auth/register.
- Open
http://localhost:4000/docs - Try
POST /auth/loginwith{"username": "admin", "password": "Admin1234"} - Copy the
accessTokenfrom the response - Click Authorize and paste the token → click Authorize
- Now you can call
GET /userandPATCH /user/:id/roledirectly from the UI
Every endpoint returns the standardized envelope. The Swagger UI displays the exact schema for both success and error responses, including field types, validation rules, and examples for each DTO.
The template ships with five Prisma schemas — switch with a single command:
npm run db:switch postgres # PostgreSQL
npm run db:switch mysql # MySQL
npm run db:switch mssql # Microsoft SQL Server
npm run db:switch mongo # MongoDB
npm run db:switch sqlite # SQLite (default)Each command copies the matching provider schema to prisma/schema.prisma and prints a DATABASE_URL hint.
Note:
npm run setupgenerates the client and pushes the schema. Run it after switching providers.
prisma/providers/
├── schema.postgres.prisma # PostgreSQL
├── schema.mysql.prisma # MySQL
├── schema.mssql.prisma # SQL Server
├── schema.mongodb.prisma # MongoDB (Δ: @db.ObjectId, no Cascade)
└── schema.sqlite.prisma # SQLite (Δ: no enums — String field)
src/
├── main.ts # Bootstrap · Helmet · Pino Logger · Swagger · Global guards
├── app.module.ts # Root module · LoggerModule · ThrottlerModule · RolesGuard
├── common/
│ ├── constants/role.ts # TS Role enum (ADMIN, MODERATOR, DONATOR, USER)
│ ├── decorators/roles.decorator.ts
│ ├── guards/roles.guard.ts # @Roles() → 403 on mismatch
│ ├── response.interceptor.ts # { success, data, message, timestamp }
│ └── http-exception.filter.ts
├── auth/
│ ├── auth.controller.ts # register · login · refresh · logout · invite-codes
│ ├── auth.service.ts # Case‑normalized register/login
│ ├── token.service.ts # JWT + refresh‑token generation, atomic rotation, cleanup
│ ├── token-cleanup.service.ts# Scheduled hourly cleanup of expired / revoked tokens
│ ├── jwt.strategy.ts # Passport strategy (sub + role in payload)
│ ├── jwt-auth.guard.ts # Global guard · @Public() bypass
│ └── dto/
│ ├── register.dto.ts
│ ├── login.dto.ts
│ └── refresh.dto.ts
├── activity/
│ ├── activity.controller.ts # POST /activity · GET /activity
│ ├── activity.service.ts # Log & list activity events per user
│ └── dto/
│ └── create-activity.dto.ts
├── user/
│ ├── user.controller.ts # profile · updateProfile · stats · list · updateRole · changePassword · delete
│ ├── user.service.ts
│ └── dto/
│ ├── change-password.dto.ts
│ ├── update-role.dto.ts
│ └── update-profile.dto.ts
├── health/
│ ├── health.controller.ts # GET /health · DB ping
│ └── health.module.ts
├── metrics/
│ ├── metrics.controller.ts # GET /metrics · Prometheus text format
│ └── metrics.module.ts
└── prisma/
├── prisma.service.ts # Singleton PrismaClient
└── prisma.module.ts
All endpoints return the standard envelope (success, data, message, timestamp).
Create an account. Requires a valid single‑use invite code.
Request
{
"username": "john_doe",
"email": "john@example.com",
"password": "Str0ng!Pass",
"inviteCode": "INVITE-2024-001"
}Validation
| Field | Rules |
|---|---|
username |
3–30 chars, alphanumeric + underscores |
email |
valid email |
password |
8–128 chars, uppercase + lowercase + digit |
inviteCode |
must exist and be unused |
Response 201
{
"success": true,
"data": { "accessToken": "…", "refreshToken": "uuid" },
"message": "Created",
"timestamp": "2026-07-09T12:00:00.000Z"
}Request
{ "username": "john_doe", "password": "Str0ng!Pass" }Response 201 — same shape as register.
Exchange a valid refresh token for a new pair. The old token is revoked (rotation).
Request
{ "refreshToken": "uuid-from-previous-response" }Response 201 — new accessToken + refreshToken.
Revoke a refresh token immediately.
Request
{ "refreshToken": "uuid-to-revoke" }Response 201
{ "success": true, "data": { "message": "Logged out" }, … }Generate one or more single-use invite codes. Requires ADMIN or MODERATOR role.
Request
{ "count": 1 }| Field | Type | Default | Rules |
|---|---|---|---|
count |
integer (optional) | 1 |
1–10 |
Response 201
{
"success": true,
"data": { "codes": ["INVITE-A1B2C3D4E5F6G7H8"] },
"message": "Created",
"timestamp": "2026-07-09T12:00:00.000Z"
}Requires Authorization: Bearer <accessToken>.
Response 200
{
"success": true,
"data": { "id": "…", "username": "john_doe", "email": "john@example.com", "role": "USER", "createdAt": "…" }
}Update the authenticated user's profile. Currently supports changing email address.
Request
{ "email": "newemail@example.com" }Response 200
{
"success": true,
"data": { "id": "…", "username": "john_doe", "email": "newemail@example.com", "role": "USER", "createdAt": "…" }
}Returns aggregate statistics for the dashboard: total users, new users in last 7 days, and role distribution.
Response 200
{
"success": true,
"data": {
"totalUsers": 5,
"newUsers7d": 2,
"roleDistribution": [
{ "role": "ADMIN", "count": 1 },
{ "role": "MODERATOR", "count": 1 },
{ "role": "USER", "count": 3 }
]
}
}List all users (paginated). @Roles(Role.ADMIN).
Query params
| Param | Type | Default | Description |
|---|---|---|---|
skip |
integer | 0 |
Records to skip |
take |
integer | 100 |
Records to take (max 100) |
Response 200
{
"success": true,
"data": {
"users": [ { "id": "…", "username": "…", "email": "…", "role": "USER", "createdAt": "…" } ],
"total": 42,
"skip": 0,
"take": 100
}
}Change a user's role. Uses DTO with @IsEnum(Role) validation.
Request
{ "role": "MODERATOR" }Change the authenticated user's password.
Request
{ "currentPassword": "Str0ng!Pass", "newPassword": "N3w!Pass456" }Delete the authenticated user's account permanently.
Response 200
{ "success": true, "data": { "message": "User deleted" }, … }Admin‑delete any user account.
Public health check that pings the database.
Response 200
{ "success": true, "data": { "status": "ok", "database": "connected" }, … }Prometheus metrics (default process metrics collected via prom-client). @Public(). Not shown in Swagger.
Log an activity event for the authenticated user (e.g. login, password change). Events are stored server-side and available via GET /api/activity.
Request
{ "type": "password_change", "detail": "Changed account password" }Validation
| Field | Rules |
|---|---|
type |
1–50 chars, required |
detail |
0–500 chars, optional |
Response 201
{
"success": true,
"data": { "id": "uuid" },
"message": "Created",
"timestamp": "2026-07-09T12:00:00.000Z"
}Return recent activity events for the authenticated user, most recent first. Default 50, max 100.
Query params
| Param | Type | Default | Description |
|---|---|---|---|
take |
integer | 50 |
Number of entries (max 100) |
Response 200
{
"success": true,
"data": [
{ "id": "uuid", "type": "password_change", "detail": "Changed account password", "createdAt": "2026-07-09T12:00:00.000Z" },
{ "id": "uuid", "type": "login", "detail": "Logged in as admin", "createdAt": "2026-07-09T11:00:00.000Z" }
]
}| Role | Tag | Permissions |
|---|---|---|
ADMIN |
🔒 | List users · Update roles · Generate invite codes |
MODERATOR |
🛡 | Generate invite codes |
DONATOR |
⭐ | (same as USER) |
USER |
👤 | Profile, login, refresh |
Seeded users (password same as username + 1234): admin / mod / donor.
curl -X POST http://localhost:4000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"Admin1234"}'Configured globally via @nestjs/throttler:
| Variable | Default | Meaning |
|---|---|---|
THROTTLE_TTL |
60 |
Window (seconds) |
THROTTLE_LIMIT |
60 |
Max requests per window |
Returns 429 Too Many Requests when exceeded.
Uses nestjs-pino with bufferLogs: true. Logs are output as newline‑delimited JSON:
{"level":30,"time":1720500000000,"pid":1,"hostname":"...","name":"NestFactory","msg":"Starting Nest application..."}Add pino-pretty for human‑readable output in development:
npm run dev | npx pino-prettyCopy .env.example to .env in the project root and adjust as needed:
cp .env.example .env| Variable | Default | Description |
|---|---|---|
API_PORT |
4000 |
HTTP port |
NODE_ENV |
development |
Environment (controls entrypoint.sh behavior — production uses migrate deploy, dev uses db push + seed) |
PUBLIC_URL |
— | Public HTTPS origin (e.g. https://api.example.com). When set, Swagger UI uses this URL for the spec, HSTS headers are disabled (trusts the upstream proxy), and trust proxy is enabled |
CORS_ORIGIN |
* |
Allowed CORS origin (set to your frontend URL in production, e.g. https://app.example.com) |
DATABASE_URL |
file:./dev.db |
Prisma datasource URL |
JWT_SECRET |
dev-secret-123 |
HMAC secret for access tokens |
JWT_EXPIRATION |
15m |
Access token TTL (ms‑format, e.g. 15m, 1h) |
THROTTLE_TTL |
60 |
Rate‑limit window (seconds) |
THROTTLE_LIMIT |
60 |
Max requests per window |
When the API runs on an EC2 instance accessed via public IP over HTTP, Swagger UI fails because browsers auto‑upgrade subresource requests to HTTPS (ERR_SSL_PROTOCOL_ERROR). The fix: set PUBLIC_URL to point requests at the public HTTPS origin.
Creates an HTTPS tunnel in one command — no account required.
# 1. Install cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
chmod +x cloudflared
sudo mv cloudflared /usr/local/bin/cloudflared
# 2. Start tunnel (keep this terminal open)
cloudflared tunnel --url http://localhost:5050Look for this in the output:
Your quick Tunnel has been created! Visit it at:
https://<random-words>.trycloudflare.com
# 3. Restart the API with PUBLIC_URL
export PUBLIC_URL=https://<random-words>.trycloudflare.com
npm run start:prodOpen https://<random-words>.trycloudflare.com/docs — Swagger UI works over HTTPS.
⚠️ Quick tunnels have no uptime guarantee. For production, use a named tunnel with a Cloudflare account: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
| Effect | Reason |
|---|---|
| Disables Helmet's HSTS header | Prevents browser from forcing HTTPS before the proxy |
Sets trust proxy |
Express trusts X-Forwarded-* headers from the proxy |
| Adds a server entry to the OpenAPI spec | "Try it out" requests hit the correct origin |
Configures Swagger's url option |
Spec loaded from the public HTTPS endpoint |
- Create an ALB with an HTTPS listener (port 443)
- Request a free ACM certificate in the same region
- Target group forwards HTTP:5050 to the EC2 instance
- Use the ALB DNS name as
PUBLIC_URL
# /etc/nginx/sites-available/api
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5050;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}sudo certbot --nginx -d api.example.com
PUBLIC_URL=https://api.example.comnpm run docker:build # build image
npm run docker:up # start api + postgres
npm run docker:down # stop everythingFirst‑run step: switch Prisma provider to PostgreSQL inside the container:
docker compose exec api npm run db:switch postgres docker compose restart api
npm run test:e2e41 tests covering registration, login, refresh‑token rotation (atomic, expired, revoked), logout, invite‑code generation, RBAC enforcement (incl. tampered JWT, DTO validation), paginated user list, case normalization, password change, user self‑deletion, admin user deletion, health check, and error‑response contract. A dedicated test.db is used and automatically cleaned up.
| Script | Action |
|---|---|
npm run build |
Compile TypeScript → dist/ |
npm run dev |
Watch mode (auto‑restart) |
npm run start:prod |
Run from dist/main.js |
npm run type-check |
tsc --noEmit (type‑check only) |
npm run lint |
tsc --noEmit (type‑check only) |
npm run setup |
prisma:generate + prisma:push + seed |
npm run seed |
Seed users (admin/mod/donor) + invite codes |
npm run test:e2e |
End‑to‑end test suite |
npm run db:switch <provider> |
Swap Prisma provider schema |
npm run docker:build |
docker compose build |
npm run docker:up |
docker compose up -d |
MIT — free for any use.