A production-grade Express.js boilerplate built for teams who want to start shipping without spending weeks on infrastructure. This is not a minimal hello-world starter — it is a fully wired system with layered clean architecture, strict TypeScript, automated security scanning, a complete CI/CD pipeline, Docker support, and structured AI agent instructions so every major coding tool understands the codebase from day one.
- Tech Stack
- Quick Start
- Project Architecture
- Request Lifecycle
- Database Design
- Authentication and Security
- Error Handling
- Middleware Pipeline
- Development Workflow
- Git Branching Strategy
- Pre-Commit Hooks
- Commit Message Convention
- CI/CD Pipeline
- Security Automation
- Testing
- Code Quality
- Containerization
- AI Agent Compatibility
- API Overview
- Configuration Reference
- Contributing and Changelog
- Contributors
- Star History
| Layer | Technology | Version |
|---|---|---|
| Runtime | Node.js | 22+ |
| Framework | Express | 5 |
| Language | TypeScript | 6 (strict) |
| ORM | Prisma | 7 |
| Database | PostgreSQL | 16 |
| Validation | Zod | 4 |
| Authentication | jsonwebtoken + bcryptjs | latest |
| Logging | Pino + pino-http | latest |
| CSRF | csrf-csrf | 4 |
| API Docs | swagger-ui-express | latest |
| Testing | Vitest + supertest | latest |
| Package Manager | pnpm | 10+ |
| Containerization | Docker + Docker Compose | latest stable |
| CI/CD | GitHub Actions | - |
# 1. Clone
git clone https://github.com/KhaledSaeed18/node-express-boilerplate.git
cd node-express-boilerplate
# 2. Install dependencies (also installs git hooks via husky)
pnpm install
# 3. Configure environment
cp .env.example .env
# Edit .env and fill in your secrets
# 4. Start the database (Docker)
docker compose up -d
# 5. Run migrations and generate the Prisma client
pnpm prisma:migrate
pnpm prisma:generate
# 6. Seed initial data (optional)
pnpm prisma:db:seed
# 7. Start the development server
pnpm devThe API will be available at http://localhost:3000/api/v1 and the interactive API documentation at http://localhost:3000/api-docs.
For a complete setup guide including environment variable details, Docker teardown, and production deployment, see CONTRIBUTING.md.
This boilerplate follows a strict layered clean architecture. Each layer communicates only with the layer directly below it, enforced by interfaces at every boundary.
graph TD
Client(["Client"])
subgraph Application["Application Layer"]
direction TB
Routes["Routes\nBaseRoute"]
Controllers["Controllers\nBaseController"]
Services["Services"]
Repositories["Repositories\nBaseRepository"]
end
subgraph Infrastructure["Infrastructure Layer"]
Prisma["Prisma ORM\n@prisma/adapter-pg"]
DB[("PostgreSQL 16")]
end
subgraph CrossCutting["Cross-Cutting"]
Container["DI Container\nSingleton"]
Middleware["Middleware Pipeline"]
Errors["Error Classes\nGlobal Handler"]
Config["Typed Config\nenv.ts"]
Logger["Pino Logger"]
end
Client --> Middleware
Middleware --> Routes
Routes --> Controllers
Controllers --> Services
Services --> Repositories
Repositories --> Prisma
Prisma --> DB
Container -.->|"wires"| Routes
Container -.->|"wires"| Controllers
Container -.->|"wires"| Services
Container -.->|"wires"| Repositories
.
├── prisma/
│ ├── schema.prisma # Data model and database config
│ ├── migrations/ # Version-controlled migration files
│ └── seed.ts # Database seed script
├── src/
│ ├── index.ts # Port binding only — no app logic
│ ├── app.ts # Express setup: middleware chain + route mounting
│ ├── config/
│ │ ├── env.ts # Typed and validated environment config
│ │ └── logger.ts # Pino logger instance (pretty dev / JSON prod)
│ ├── container/
│ │ └── index.ts # DI Container singleton — only place that calls new
│ ├── controllers/
│ │ ├── base.controller.ts # sendResponse, sendPaginatedResponse, handleError
│ │ ├── auth.controller.ts
│ │ └── note.controller.ts
│ ├── database/
│ │ └── prismaClient.ts # Prisma client initialization with pg adapter
│ ├── docs/
│ │ ├── index.ts # OpenAPI spec assembly
│ │ ├── schemas.ts # Reusable response/request schemas
│ │ ├── setup.ts # swagger-ui-express router
│ │ └── paths/ # Per-resource path definitions
│ ├── errors/
│ │ └── index.ts # AppError hierarchy
│ ├── lib/
│ │ └── validate.ts # validateBody() Zod middleware factory
│ ├── middleware/
│ │ ├── auth.middleware.ts # JWT protect guard
│ │ ├── correlation.middleware.ts # Correlation ID injection
│ │ ├── csrf.middleware.ts # CSRF token generation
│ │ ├── error.middleware.ts # Global error handler
│ │ ├── httpLogger.middleware.ts # pino-http request logging
│ │ ├── limiter.middleware.ts # Per-route rate limiters
│ │ └── pagination.middleware.ts # Pagination query parser
│ ├── repository/
│ │ ├── base.repository.ts # findManyWithPagination, count, handlePrismaError
│ │ ├── user.repository.ts
│ │ └── note.repository.ts
│ ├── routes/
│ │ ├── base.route.ts # Container initialization, router setup
│ │ ├── auth.routes.ts
│ │ ├── note.routes.ts
│ │ └── health.routes.ts
│ ├── services/
│ │ ├── auth.service.ts
│ │ └── note.service.ts
│ ├── types/
│ │ └── index.ts # DTOs, AuthPayload, shared interfaces
│ ├── utils/
│ │ ├── generateToken.ts # JWT access + refresh token generation
│ │ ├── userNames.ts # Deterministic username generator
│ │ └── time.ts # Time utility helpers
│ └── validations/
│ ├── auth.validation.ts
│ └── note.validation.ts
├── tests/
│ ├── unit/ # Service-layer tests with mocked repositories
│ ├── integration/ # Full HTTP cycle tests via supertest
│ └── helpers/ # globalSetup, setup, test utilities
├── .claude/
│ └── skills/ # Claude Code slash-command workflows
├── .cursor/rules/ # Cursor IDE rules
├── .github/
│ ├── workflows/ # CI, CodeQL, Security pipelines
│ ├── copilot-instructions.md
│ └── ISSUE_TEMPLATE/
├── docker-compose.yml # Development database (port 5433)
├── docker-compose.test.yml # Ephemeral test database (port 5434)
├── CLAUDE.md # Claude Code instructions
├── GEMINI.md # Gemini CLI instructions
├── AGENTS.md # OpenAI Codex / ChatGPT instructions
└── .windsurfrules # Windsurf IDE instructions
Container is a singleton (src/container/index.ts) and is the only place in the entire codebase where new Repository/Service/Controller() is called. Routes access instances through getter methods, keeping all construction logic in one traceable location.
The initialization order is always: Prisma client → Repositories → Services → Controllers.
Every inbound request passes through the following stages before a response is sent:
sequenceDiagram
participant C as Client
participant Corr as Correlation ID
participant Log as pino-http
participant Helm as Helmet
participant CORS as CORS
participant Parse as cookie-parser
participant CSRF as CSRF Guard
participant Auth as protect (JWT)
participant Zod as Zod Validation
participant Ctrl as Controller
participant Svc as Service
participant Repo as Repository
participant DB as PostgreSQL
C->>Corr: HTTP request
Corr->>Log: inject x-correlation-id
Log->>Helm: log request start
Helm->>CORS: set security headers
CORS->>Parse: check origin
Parse->>CSRF: parse signed cookies
CSRF->>Auth: verify x-csrf-token
Auth->>Zod: verify JWT from cookie
Zod->>Ctrl: validate request body
Ctrl->>Svc: call service method
Svc->>Repo: business logic
Repo->>DB: Prisma query
DB-->>Repo: result rows
Repo-->>Svc: entity
Svc-->>Ctrl: response DTO
Ctrl-->>C: JSON response
On error at any stage, the middleware calls next(error) and the global error handler in src/middleware/error.middleware.ts formats the response consistently based on the thrown AppError subclass.
erDiagram
User {
String id PK "CUID"
String username UK
String email UK
String password "bcrypt hash"
DateTime createdAt
DateTime updatedAt
}
Note {
String id PK "CUID"
String title
String content
String userId FK
DateTime createdAt
DateTime updatedAt
}
User ||--o{ Note : "owns"
- All primary keys use
cuid()for collision-resistant, sortable, non-sequential IDs. notes.userIdcarriesonDelete: Cascade— deleting a user removes all their notes atomically.@@index([userId])and@@index([title])on theNotemodel cover the most common query patterns.- Prisma generates the client into
src/generated/prisma/in CJS module format. - Table names are mapped to snake_case (
@@map("users"),@@map("notes")).
All schema changes go through Prisma migrations. Migration files are version-controlled and applied in order. The production command pnpm prisma:migrate:deploy is non-interactive and safe for CI/CD pipelines.
pnpm prisma:migrate # Create and apply (dev)
pnpm prisma:migrate:deploy # Apply pending only (production / CI)
pnpm prisma:migrate:reset # Wipe and re-run all (dev reset)
pnpm prisma:generate # Regenerate client after schema changes
pnpm prisma:studio # GUI at http://localhost:5555Access tokens are short-lived (default 15 minutes). Refresh tokens are long-lived (default 7 days). Both are stored exclusively in signed HttpOnly cookies — they are never exposed in response bodies or accessible via JavaScript.
sequenceDiagram
participant C as Client
participant MW as Middleware
participant Auth as AuthController
participant Svc as AuthService
participant DB as Database
Note over C,DB: Sign In
C->>Auth: POST /api/v1/auth/signin
Auth->>Svc: signIn(email, password)
Svc->>DB: findByEmail
DB-->>Svc: user record
Svc->>Svc: bcrypt.compare(password, hash)
Svc->>Svc: sign accessToken (JWT_SECRET, 15m)
Svc->>Svc: sign refreshToken (JWT_REFRESH_SECRET, 7d)
Auth-->>C: Set-Cookie: accessToken (HttpOnly, Signed)
Auth-->>C: Set-Cookie: refreshToken (HttpOnly, Signed)
Note over C,DB: Authenticated Request
C->>MW: request + cookies
MW->>MW: protect — read signedCookies.accessToken
MW->>MW: jwt.verify(token, JWT_SECRET)
MW->>Auth: req.user = { id, email }
Note over C,DB: Token Refresh
C->>Auth: POST /api/v1/auth/refresh-token
Auth->>Svc: refreshToken(signedCookie)
Svc->>Svc: jwt.verify(token, JWT_REFRESH_SECRET)
Svc->>DB: findById(userId)
Svc->>Svc: sign new accessToken
Auth-->>C: Set-Cookie: accessToken (new, HttpOnly)
Note over C,DB: CSRF Protection
C->>Auth: GET /api/v1/auth/csrf-token
Auth-->>C: csrfToken + CSRF cookie (signed)
C->>MW: POST /api/v1/note + x-csrf-token header
MW->>MW: doubleCsrfProtection — compare header vs cookie signature
MW-->>C: 403 if mismatch, proceed if valid
All state-mutating endpoints (POST, PUT, PATCH, DELETE) are protected by csrf-csrf. The client must:
- Call
GET /api/v1/auth/csrf-tokento receive a token. - Include it as the
x-csrf-tokenrequest header on every mutating request.
The middleware validates the token by comparing a signed hash (keyed with COOKIE_SECRET) stored in a cookie against the submitted header value. The Swagger UI at /api-docs is mounted before CSRF middleware so its GET requests are not challenged.
helmet is applied to every route. The /api-docs route uses a relaxed Content Security Policy that permits the inline scripts and styles required by swagger-ui-express. All other routes use the strict default policy.
Strict-Transport-Security
X-Content-Type-Options: nosniff
X-Frame-Options: SAME-ORIGIN
X-XSS-Protection: 0 (disabled — CSP is the correct defense)
Content-Security-Policy: default-src 'self'
Each route group has its own express-rate-limit instance configured in src/middleware/limiter.middleware.ts. Limits are applied per real client IP, with trust proxy: 1 set so the correct IP is read from the X-Forwarded-For header when running behind a reverse proxy.
All errors are thrown as instances of AppError subclasses and caught by the global handler in src/middleware/error.middleware.ts. Controllers never call res.status().json() directly on error paths.
graph TD
Error["Error (native)"]
AppError["AppError\nstatusCode, isOperational"]
ValidationError["ValidationError\n400"]
AuthenticationError["AuthenticationError\n401"]
AuthorizationError["AuthorizationError\n403"]
NotFoundError["NotFoundError\n404"]
ConflictError["ConflictError\n409"]
TooManyRequestsError["TooManyRequestsError\n429"]
InternalServerError["InternalServerError\n500"]
Error --> AppError
AppError --> ValidationError
AppError --> AuthenticationError
AppError --> AuthorizationError
AppError --> NotFoundError
AppError --> ConflictError
AppError --> TooManyRequestsError
AppError --> InternalServerError
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{ "field": "email", "message": "Invalid email format" }
]
}The stack field is included only when NODE_ENV=development. isOperational: false errors (unexpected crashes) are logged at error level by Pino and return a generic 500 response without internal details.
BaseRepository.handlePrismaError() intercepts Prisma client known request errors and converts them to typed AppError subclasses before they propagate. P2025 (record not found) becomes NotFoundError, P2002 (unique constraint violation) becomes ConflictError, and so on.
The middleware chain in src/app.ts runs in this exact order for every request:
flowchart LR
A[correlationMiddleware\ninject x-correlation-id] -->
B[httpLogger\npino-http structured log] -->
C[helmet\nsecurity headers] -->
D[cors\ncheck origin] -->
E[express.json\nparse body] -->
F[cookie-parser\nparse signed cookies] -->
G[API Docs mount\n/api-docs bypass CSRF] -->
H[doubleCsrfProtection\nverify x-csrf-token] -->
I[Routes\nper-route middleware]
Per-route middleware applied inside route handlers:
| Middleware | Applied to |
|---|---|
protect |
All authenticated endpoints |
rateLimiter |
Every route group |
doubleCsrfProtection |
All mutating routes |
Zod validateBody() |
POST / PUT / PATCH routes |
flowchart TD
A[Clone repo] --> B[pnpm install]
B --> C[cp .env.example .env]
C --> D[docker compose up -d\nPostgreSQL on port 5433]
D --> E[pnpm prisma:migrate]
E --> F[pnpm prisma:generate]
F --> G[pnpm dev\nhot-reload on save]
G --> H[create feat/ branch off dev]
H --> I[implement changes]
I --> J[pnpm full-check\nformat + lint + type-check]
J --> K[git commit\nhooks enforce style + lint]
K --> L[push to origin/feat/...]
L --> M[open PR targeting dev]
M --> N[CI pipeline runs]
N --> O[merge to dev]
O --> P[open PR targeting main]
P --> Q[merge to main\nprotected branch]
Q --> R[deploy]
pnpm dev # Start with hot reload (nodemon)
pnpm build # Compile TypeScript to dist/
pnpm start # Run compiled output
pnpm full-check # format:check + lint:check + type-check
pnpm format # Prettier auto-fix
pnpm lint # ESLint auto-fix
pnpm type-check # tsc --noEmit (src)
pnpm type-check:test # tsc --noEmit (tests)
pnpm type-check:node # tsc --noEmit (prisma config)
pnpm prisma:migrate # Create and apply migration (dev)
pnpm prisma:migrate:deploy # Apply pending migrations (production)
pnpm prisma:migrate:reset # Wipe and re-run all migrations
pnpm prisma:generate # Regenerate Prisma client
pnpm prisma:studio # GUI at http://localhost:5555
pnpm prisma:db:seed # Seed initial data
pnpm prisma:validate # Validate schema file
pnpm test # Unit tests
pnpm test:watch # Unit tests in watch mode
pnpm test:coverage # Unit tests with coverage report
pnpm test:integration # Integration tests (requires test DB)
pnpm test:all # All test suites
pnpm db:test:up # Start test database (Docker, port 5434)
pnpm db:test:down # Stop and remove test database
pnpm db:test:migrate # Apply migrations to test databasemain is the production branch and is fully protected — no direct pushes are allowed. All work flows through dev first.
gitGraph
commit id: "initial commit"
branch dev
checkout dev
branch feat/zod-validation
checkout feat/zod-validation
commit id: "feat: Zod env config"
commit id: "feat: auth validation"
checkout dev
merge feat/zod-validation id: "PR merge"
branch feat/csrf-protection
checkout feat/csrf-protection
commit id: "feat: csrf-csrf setup"
commit id: "feat: CSRF token endpoint"
checkout dev
merge feat/csrf-protection id: "PR merge 2"
branch fix/pagination-logic
checkout fix/pagination-logic
commit id: "fix: NoteController pagination"
checkout dev
merge fix/pagination-logic id: "PR merge 3"
checkout main
merge dev id: "release to main"
commit id: "chore: tag release"
| Branch pattern | Purpose |
|---|---|
main |
Production, protected |
dev |
Integration target for all work |
feat/<description> |
New features |
fix/<description> |
Bug fixes |
refactor/<description> |
Refactoring |
docs/<description> |
Documentation |
chore/<description> |
Maintenance |
ci/<description> |
CI/CD changes |
Git hooks are managed by husky and installed automatically on pnpm install. Every commit passes through two hooks:
flowchart TD
A["git commit -m '...'"] --> B
subgraph PreCommit["pre-commit hook (lint-staged)"]
B[staged .ts files] --> C["prettier --write"]
C --> D["eslint --fix"]
end
D --> E
subgraph CommitMsg["commit-msg hook (commitlint)"]
E[commit message] --> F{follows Conventional\nCommits format?}
F -->|yes| G[commit created]
F -->|no| H[commit rejected\nwith error message]
end
lint-staged runs only on staged files, so large codebases are not penalized. commitlint validates against @commitlint/config-conventional.
This project enforces Conventional Commits via commitlint. The format is:
<type>(<optional scope>): <subject>
<optional body>
<optional footer>
| Type | When to use |
|---|---|
feat |
New feature |
fix |
Bug fix |
docs |
Documentation only |
style |
Formatting, whitespace |
refactor |
No fix, no feature |
perf |
Performance improvement |
test |
Tests |
build |
Build system or dependencies |
ci |
CI configuration |
chore |
Maintenance |
revert |
Reverts a prior commit |
Rules enforced: lowercase type, no trailing period, no title-case subject, header max 100 characters, body lines max 300 characters.
Three independent GitHub Actions workflows run on every push and pull request.
flowchart TD
Trigger["Push or Pull Request\nto main or dev"] --> Q & CQ & Sec
subgraph Q["ci.yml — Quality + Test"]
direction TB
Q1["format:check"] --> Q2["lint:check"]
Q2 --> Q3["type-check (src + tests + node)"]
Q3 --> Q4["pnpm audit (high+)"]
Q4 --> Q5["pnpm build"]
Q5 --> Q6["Start PostgreSQL 16 service"]
Q6 --> Q7["pnpm test (unit)"]
Q7 --> Q8["pnpm test:integration"]
end
subgraph CQ["codeql.yml — Static Analysis"]
CQ1["CodeQL initialize\n(javascript-typescript)"]
CQ2["Auto build"]
CQ3["CodeQL analyze\n(security-and-quality suite)"]
CQ1 --> CQ2 --> CQ3
end
subgraph Sec["security.yml — Security Scanning"]
S1["TruffleHog\nscan git diff for verified secrets"]
S2["Dependency Review\nblock high+ CVE packages on PR"]
end
The quality job and test job in ci.yml run sequentially — tests only execute if the quality checks pass. The CodeQL and security workflows run in parallel with CI.
All workflows use concurrency groups with cancel-in-progress: true so redundant runs are cancelled immediately when a new push supersedes them.
CodeQL runs on every push, pull request, and on a weekly schedule (Mondays at 03:00 UTC). The security-and-quality query suite covers SQL injection, path traversal, prototype pollution, and dozens of other vulnerability classes. A custom suppression configuration in .github/codeql/codeql-config.yml silences the false positive that CodeQL raises on the CSRF token endpoint.
TruffleHog scans the git diff on every push and pull request, checking only newly introduced commits. The --only-verified flag means it actively verifies that discovered credentials actually authenticate against the target service before flagging them, keeping false-positive noise low.
On every pull request, the dependency review action compares the dependency graph before and after the PR and surfaces packages with CVSS scores of high or above (sourced from the GitHub Advisory Database). It posts an inline PR comment summarizing flagged packages and fails the check if any are found.
flowchart LR
D["Dependabot\nweekly schedule"] --> NB["npm packages\nnon-breaking only"]
D --> GA["GitHub Actions\nmajor versions excluded"]
NB --> PR1["opens PR to dev"]
GA --> PR2["opens PR to dev"]
PR1 --> CI["CI pipeline runs"]
PR2 --> CI
CI -->|pass| Merge["merge or review"]
pnpm audit also runs in the CI quality job with --audit-level=high, catching vulnerabilities in already-installed packages before a deployment.
The test suite is split into two independent layers. Both use Vitest and are configured separately.
flowchart TD
subgraph Unit["Unit Tests — tests/unit/"]
UT1["Service under test"]
UT2["I<Name>Repository\nmocked with vi.fn()"]
UT3["Vitest assertions"]
UT1 --> UT2
UT1 --> UT3
end
subgraph Integration["Integration Tests — tests/integration/"]
IT1["supertest HTTP client"]
IT2["Express app\n(imported, no port binding)"]
IT3["Real PostgreSQL 16\nvia Docker or CI service"]
IT4["Vitest assertions"]
IT1 --> IT2
IT2 --> IT3
IT1 --> IT4
end
subgraph Setup["Test Infrastructure"]
S1["docker-compose.test.yml\nPostgreSQL RAM disk port 5434"]
S2["pnpm db:test:migrate\napply migrations"]
S3["globalSetup.ts\nbeforeAll / afterAll"]
S4["setup.ts\nper-test cleanup"]
end
Setup --> Integration
Unit tests cover the service layer in isolation. The repository is replaced with a full vi.fn() mock, so no database connection is required. Tests run with pnpm test and complete in milliseconds.
Coverage thresholds are enforced by Vitest:
| Metric | Threshold |
|---|---|
| Lines | 80% |
| Functions | 85% |
| Branches | 75% |
| Statements | 80% |
Integration tests cover the full HTTP cycle from the HTTP method and path down to the database and back. They use supertest against the real Express application (imported without port binding) connected to a dedicated test PostgreSQL instance.
The test database uses in-memory storage so it is wiped clean on every container restart. fileParallelism: false serializes test files to prevent concurrent mutations on the shared test schema.
Coverage thresholds for integration tests:
| Metric | Threshold |
|---|---|
| Lines | 70% |
| Functions | 75% |
| Branches | 65% |
| Statements | 70% |
Every integration test validates at minimum: valid request (happy path), invalid input (400), unauthenticated request (401), missing CSRF token (403), and resource not found (404).
The following tools are configured and enforced:
| Tool | Configuration | When it runs |
|---|---|---|
| Prettier | .prettierrc |
pre-commit (lint-staged), CI |
| ESLint | eslint.config.mjs |
pre-commit (lint-staged), CI |
| TypeScript | tsconfig.json (strict) |
pre-commit (type-check), CI |
| commitlint | commitlint.config.cjs |
commit-msg hook |
| Vitest | vitest.unit.config.ts / vitest.integration.config.ts |
CI test job |
ESLint rules enforced beyond the standard recommended set:
- No
anytype — useunknownand narrow explicitly - All type-only imports must use
import type - Explicit return types on all public methods
- No floating promises — all async calls must be awaited or rejection handled
- Prefer
??over||for nullish coalescing
Formatting conventions: 4-space indent, single quotes, semicolons, trailing commas, 100-character line limit.
docker-compose.yml starts a PostgreSQL 16 instance on port 5433 (deliberately offset from the default 5432 to avoid conflicts with any locally installed Postgres).
docker compose up -d # start in background
docker compose down # stop, keep volume
docker compose down -v # stop and delete volumedocker-compose.test.yml starts a separate PostgreSQL 16 instance on port 5434 using in-memory storage. The database is wiped every time the container restarts, giving each test run a guaranteed clean state.
pnpm db:test:up # start test DB
pnpm db:test:migrate # apply migrations to test DB
pnpm test:integration # run tests against it
pnpm db:test:down # stop and removeThe included Dockerfile is a multi-stage build. The application compiles TypeScript in the build stage and runs the compiled output from dist/ in the final stage. Prisma migrations are applied separately via pnpm prisma:migrate:deploy before starting the container.
This boilerplate is structured to be understood by AI coding tools out of the box. Rather than leaving agents to infer architecture from code exploration, every major tool has a dedicated instruction file that communicates the system design, hard rules, and code patterns it needs to generate correct contributions without review cycles.
flowchart LR
Codebase["Codebase"] --> C["CLAUDE.md\n+ .claude/skills/"]
Codebase --> G["GEMINI.md"]
Codebase --> A["AGENTS.md"]
Codebase --> CU["cursor/rules/*.mdc"]
Codebase --> CO[".github/copilot-instructions.md"]
Codebase --> W["windsurfrules\n(Windsurf)"]
C --> ClaudeCode["Claude Code"]
G --> Gemini["Google Gemini CLI"]
A --> OpenAI["OpenAI Codex / ChatGPT"]
CU --> Cursor["Cursor"]
CO --> Copilot["GitHub Copilot"]
W --> Windsurf["Windsurf"]
Each file teaches the agent the same core knowledge: the layered architecture, the DI container pattern, the hard rules (no process.env, no console.*, no any, no direct res.json() on errors), the Zod validation pattern, and the correct way to throw errors.
Claude Code has four slash-command skills in .claude/skills/ that automate the most common multi-step tasks:
| Skill | What it does |
|---|---|
/new-resource |
Scaffolds a complete resource across all 8 layers — schema, types, validation, repository, service, controller, route, container wiring, OpenAPI, and tests |
/add-middleware |
Creates a middleware file and wires it into the pipeline at the correct position |
/add-test |
Scaffolds unit tests and integration tests for an existing resource |
/update-schema |
Edits the Prisma schema, runs migration, regenerates the client, and updates all affected layers |
This means an agent can add a fully wired, tested, documented resource to the API by running a single skill instead of constructing the sequence from first principles each time.
The full interactive API documentation is available at http://localhost:3000/api-docs when the server is running. The OpenAPI spec is maintained in src/docs/.
Base URL: http://localhost:3000/api/v1
| Group | Endpoints |
|---|---|
| Health | GET /health |
| Auth | GET /auth/csrf-token, POST /auth/signup, POST /auth/signin, POST /auth/refresh-token, POST /auth/logout |
| Notes | POST /note, GET /note, GET /note/:id, PUT /note/:id, DELETE /note/:id |
All mutating endpoints require a valid CSRF token passed as x-csrf-token. All note endpoints require a valid access token cookie from a prior sign-in.
Standard response envelope:
{
"statusCode": 200,
"message": "Notes retrieved successfully",
"data": [],
"pagination": {
"totalItems": 42,
"totalPages": 5,
"currentPage": 1,
"pageSize": 10,
"hasNext": true,
"hasPrevious": false
}
}Copy .env.example to .env before running locally.
| Variable | Required | Default | Description |
|---|---|---|---|
NODE_ENV |
No | development |
Controls log format, error detail, and security options |
PORT |
No | 3000 |
HTTP server port |
API_VERSION |
No | v1 |
Injected into every route prefix |
BASE_URL |
No | /api |
Route prefix |
DATABASE_URL |
Yes | - | Full PostgreSQL connection string |
JWT_SECRET |
Yes | - | Minimum 32 characters |
JWT_REFRESH_SECRET |
Yes | - | Minimum 32 characters, different from JWT_SECRET |
JWT_EXPIRE_TIME |
No | 15m |
Access token TTL (zeit/ms format) |
JWT_REFRESH_EXPIRE_TIME |
No | 7d |
Refresh token TTL (zeit/ms format) |
CLIENT_URL |
Yes | - | Allowed CORS origin |
BCRYPT_SALT_ROUNDS |
No | 12 |
bcrypt cost factor (10-15) |
COOKIE_SECRET |
Yes | - | Minimum 32 characters, used for cookie signing and CSRF token verification |
All variables are validated at startup via Zod in src/config/env.ts. The application exits immediately with a descriptive error if any required variable is missing or fails its constraint.
For setup instructions, branch rules, commit format, testing setup, and the full pre-merge checklist, see CONTRIBUTING.md.
For a detailed record of every change made during the 2026 revival including dependency upgrades, security additions, and architectural changes, see CHANGELOG.md.
To report a bug or request a feature, use the issue templates in .github/ISSUE_TEMPLATE/.