Production-ready backend starter using NduloJS + Elysia + Drizzle ORM + Valkey (Redis-compatible) + PostgreSQL.
| Layer |
Tech |
| Runtime |
Bun |
| Framework |
NduloJS (Elysia adapter) |
| Database |
PostgreSQL 16 via Drizzle ORM |
| Cache / Queue |
Valkey 8 (Redis-compatible) |
| Auth |
JWT + refresh tokens |
| Validation |
Zod + TypeBox |
| Observability |
Pino structured logs + Prometheus metrics |
| API Docs |
Swagger via @elysiajs/swagger |
cp .env.example .env
docker compose up -d
Postgres will be available on localhost:5432, Valkey on localhost:6379.
bun run db:generate
bun run db:migrate
Server: http://localhost:3000
Swagger: http://localhost:3000/docs
Metrics: http://localhost:9091/metrics
# App
PORT=3000
METRICS_PORT=9091
NODE_ENV=development
LOG_LEVEL=info
LOG_PRETTY=true
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=app
# Valkey / Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=secret
# Auth
JWT_SECRET=change-me-in-production
JWT_EXPIRES_IN=15m
REFRESH_TOKEN_EXPIRES_IN=7d
# CORS
CORS_ORIGINS=http://localhost:5173
CORS_CREDENTIALS=true
src/
├── config/ # env validation
├── db/ # drizzle schema + migrations
├── middlewares/ # auth, rate-limit, metrics
├── modules/
│ └── users/
│ ├── application/ # services, ports, DTOs
│ ├── domain/ # entities
│ └── infrastructure/
│ ├── db/ # drizzle repository
│ └── http/ # controllers
└── shared/
├── auth/ # JWT helpers
├── logger/ # pino wrapper
├── metrics/ # prometheus
└── upload/ # multipart parser
| Method |
Path |
Description |
| POST |
/users/auth/register |
Create account |
| POST |
/users/auth/login |
Login |
| POST |
/users/auth/logout |
Logout |
| POST |
/users/auth/refresh |
Refresh access token |
| Method |
Path |
Description |
| GET |
/users/me |
Get current user |
| PATCH |
/users/me |
Update profile |
| POST |
/users/me/avatar |
Upload avatar |
| GET |
/users |
List users |
| DELETE |
/users/:id |
Delete user |
| Method |
Path |
Description |
| GET |
/sessions |
List active sessions |
| DELETE |
/sessions/:id |
Revoke session |
| POST |
/sessions/revoke-all |
Revoke all sessions |
| Method |
Path |
Description |
| GET |
/health |
Health check |
| GET |
/ready |
Readiness check |
| GET |
/docs |
Swagger UI |
| GET |
/metrics |
Prometheus metrics |
bun run dev # dev server with watch
bun run build # production build
bun run start # run built output
bun run typecheck # tsc --noEmit
bun run db:generate # generate drizzle migrations
bun run db:migrate # run migrations
bun run db:push # push schema (dev only)
bun run db:studio # drizzle studio UI
bun run db:seed # seed database
bun run test # vitest
All handlers return Ok(data) or Err(ErrorFactory.xxx(...)) — never throw. NduloJS maps these to correct HTTP status codes automatically.
import { Ok, Err, ErrorFactory } from 'ndulojs';
// 200
return Ok({ user });
// 404
return Err(ErrorFactory.notFound('User not found', 'User', userId));
// 401
return Err(ErrorFactory.unauthorized('Invalid token', 'invalid_token'));
// 409
return Err(ErrorFactory.conflict('Email already registered', 'email'));
// 422
return Err(ErrorFactory.validation('Invalid input', errors));