@bilikaz/nestjs-starter-boilerplate
v1.0.0
Published
Minimal NestJS boilerplate with TypeORM, JWT/OpenID authentication, and OpenAPI (Swagger) documentation out of the box
Readme
NestJS Starter
Minimal NestJS boilerplate with TypeORM, JWT/OpenID authentication, and OpenAPI (Swagger) documentation out of the box.
Stack
- NestJS v11 — modular, decorator-based Node.js framework
- TypeORM 0.3 — ORM with MySQL driver, snake_case naming strategy, migration support
- Authentication — JWT access/refresh tokens, local (username+password) strategy via Passport; roles-based access control (
@Roles,@Public,@OrganizationRolesdecorators) with composable OR-guard support viaAnyGuard - OpenAPI — Swagger UI auto-generated from decorators, available at
/api
Getting started
npm install
cp demo.env .env # fill in real values
npm run start:devEnvironment variables
Copy demo.env to .env and adjust the values.
| Variable | Description | Default |
|---|---|---|
| APP_PORT | HTTP port the server listens on | 3000 |
| APP_TITLE | Swagger UI title | Example |
| APP_DESCRIPTION | Swagger UI description | description |
| APP_VERSION | Swagger UI version | 1.0 |
| DATABASE_HOST | MySQL host | — |
| DATABASE_PORT | MySQL port | — |
| DATABASE_USER | MySQL username | — |
| DATABASE_PASSWORD | MySQL password | — |
| DATABASE_NAME | MySQL database name | — |
| JWT_SECRET | Secret used to sign JWT tokens | — |
| JWT_EXPIRATION | Access token lifetime in seconds | 900 |
| JWT_REFRESH_EXPIRATION | Refresh token lifetime in seconds | 604800 |
| OIDC_ISSUER | JWT iss claim value | — |
| OIDC_AUDIENCE | JWT aud claim value | api |
Scripts
npm run start:dev # development with watch mode
npm run start:debug # debug mode with Node inspector
npm run build # compile TypeScript to dist/
npm run start:prod # run compiled build
npm test # unit tests
npm run test:watch # unit tests in watch mode
npm run test:cov # unit tests with coverage report
npm run test:e2e # end-to-end tests
npm run lint # ESLint with auto-fix
npm run format # Prettier formatting
npm run seed:create -- -n db/seeds/DescriptiveName # create a new seed file
npm run seed:run # run all seeds
npm run migration:generate -- db/migrations/DescriptiveName # generate migration from entity changes
npm run migration:run # apply pending migrations
npm run migration:revert # revert last migration
npm run migration:show # show migration statusDatabase migrations
Migrations live in db/migrations/. The datasource is configured in db/datasource.ts. Auto-synchronize is disabled — all schema changes must go through migrations.
# Generate a new migration from entity changes
npm run migration:generate -- db/migrations/DescriptiveName
# Apply all pending migrations
npm run migration:run
# Revert the last applied migration
npm run migration:revert
# Show migration status (applied / pending)
npm run migration:showThe migration commands use
typeorm-ts-node-commonjsand loaddb/datasource.tsdirectly, so they pick up the.envfile automatically.
Database seeding
Seeding uses typeorm-extension. Seed files live in db/seeds/. Whether a seed is idempotent depends on its track attribute — seeds with track: true are only run once and skipped on subsequent runs.
# Create a new seed file
npm run seed:create -- -n db/seeds/DescriptiveName
# Run all seeds
npm run seed:runThe initial seed creates an admin user:
| Field | Value |
|---|---|
| username | admin |
| email | admin@localhost |
| password | admin |
| role | admin |
Claude Code
This project includes a Claude Code skill for scaffolding new CRUD modules. With Claude Code CLI installed, run:
/createEntity <EntityName> <field:type[:options]> ...This generates a complete module — entity, DTOs, repository, service, controller, and module file — and wires it into AppModule. All controller actions are protected with @Roles(UserRole.ADMIN) by default. Use role flags to override per-action:
# All actions require ADMIN role (default)
/createEntity Product name:string price:decimal
# Custom roles per action
/createEntity Product name:string --create-roles=ADMIN --list-roles=USER,ADMIN --get-public
# All actions public (no auth required)
/createEntity Product name:string --no-rolesSee .claude/skills/createEntity/SKILL.md for the full field syntax and all available role flags.
To scaffold a new guard that integrates with the AnyGuard OR-composition system, use:
/createGuard <GuardName> <description of what it restricts>This generates the decorator and guard class (with anyGuard protocol wired in), registers it in AuthModule, and appends it to the global AnyGuard(...) call in AppModule. Pass --no-any-guard to skip the last step.
# Guard that checks a user's subscription tier
/createGuard SubscriptionTier restricts routes to users with a minimum subscription tier
# Same, but don't add it to the global AnyGuard automatically
/createGuard SubscriptionTier restricts routes to users with a minimum subscription tier --no-any-guardSee .claude/skills/createGuard/SKILL.md for full details and the AnyGuard protocol contract.
Authorization
Global role guard — @Roles
All routes are protected by default. Use @Roles(UserRole.X) to restrict a route to specific system-level roles, or @Public() to opt out entirely.
@Roles(UserRole.ADMIN) // only admins
@Roles(UserRole.USER, UserRole.ADMIN) // either role
@Public() // no auth requiredOrganization-scoped guard — @OrganizationRoles
OrganizationRoleGuard enforces membership and optional role within an organization. It reads organizationId from the route params (configurable via the decorator's param option).
@OrganizationRoles() // any member
@OrganizationRoles(OrganizationUserRole.OWNER) // owner only
@OrganizationRoles(OrganizationUserRole.OWNER, OrganizationUserRole.MANAGER)
// Custom param name (default is 'organizationId')
@OrganizationRoles(OrganizationUserRole.OWNER, { param: 'orgId' })Composable OR logic — AnyGuard
By default NestJS applies guards with AND logic — all must pass. AnyGuard is a factory that wraps multiple guards and passes if any one of them allows the request (OR logic). The globally registered instance combines RoleGuard and OrganizationRoleGuard:
// app.module.ts
{ provide: APP_GUARD, useClass: AnyGuard(RoleGuard, OrganizationRoleGuard) }This means a route annotated with both decorators is accessible to system admins or organization owners — whichever check passes first:
@Roles(UserRole.ADMIN)
@OrganizationRoles(OrganizationUserRole.OWNER)
@Get(':organizationId/settings')
getSettings() { ... }How guards signal their result to AnyGuard — each compatible guard exposes an anyGuard: boolean property:
| anyGuard value | No decorator on route | Check passes | Check fails |
|---|---|---|---|
| false (standalone default) | true — transparent | true | false |
| true (set by AnyGuard) | false — not applicable | true | throws ForbiddenException |
AnyGuard resolves as follows:
- Any guard returns
true→ allowed - All guards returned
false(none applicable) → allowed (route has no restriction) - At least one threw and none returned
true→ forbidden
Adding a custom guard compatible with AnyGuard
Any guard can participate in OR composition by following the same contract as OrganizationRoleGuard:
- Add a public
anyGuard = falseproperty. - When
anyGuardistrue: returnfalseif your decorator is absent, throwForbiddenExceptionif the check fails. - Pass the class to
AnyGuard(...)inapp.module.ts.
Swagger / OpenAPI
Once the app is running, the interactive API documentation is available at:
http://localhost:<APP_PORT>/apiAll endpoints are documented with request/response schemas. Protected endpoints require a Bearer token — click Authorize in the Swagger UI and paste a JWT access token obtained from POST /auth/login.
