{"_id":"@bilikaz/nestjs-starter-boilerplate","name":"@bilikaz/nestjs-starter-boilerplate","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@bilikaz/nestjs-starter-boilerplate","version":"1.0.0","description":"Minimal NestJS boilerplate with TypeORM, JWT/OpenID authentication, and OpenAPI (Swagger) documentation out of the box","author":{"name":"bilikaz"},"private":false,"license":"MIT","scripts":{"build":"nest build","format":"prettier --write \"src/**/*.ts\" \"test/**/*.ts\"","start":"nest start","start:dev":"nest start --watch","start:debug":"nest start --debug --watch","start:prod":"node dist/main","lint":"eslint \"{src,apps,libs,test}/**/*.ts\" --fix","test":"jest","test:watch":"jest --watch","test:cov":"jest --coverage","test:debug":"node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand","test:e2e":"jest --config ./test/jest-e2e.json","typeorm":"typeorm-ts-node-commonjs -d db/datasource.ts","migration:generate":"npm run typeorm -- migration:generate","migration:run":"npm run typeorm -- migration:run","migration:revert":"npm run typeorm -- migration:revert","migration:show":"npm run typeorm -- migration:show","typeorm-extension":"ts-node ./node_modules/typeorm-extension/bin/cli.cjs","seed:run":"npm run typeorm-extension -- seed:run -d db/datasource.ts","seed:create":"npm run typeorm-extension -- seed:create"},"dependencies":{"@nestjs/common":"^11.0.1","@nestjs/config":"^4.0.3","@nestjs/core":"^11.0.1","@nestjs/jwt":"^11.0.2","@nestjs/mapped-types":"*","@nestjs/passport":"^11.0.5","@nestjs/platform-express":"^11.0.1","@nestjs/swagger":"^11.2.6","@nestjs/typeorm":"^11.0.0","bcrypt":"^6.0.0","class-transformer":"^0.5.1","class-validator":"^0.14.4","mysql2":"^3.20.0","passport":"^0.7.0","passport-jwt":"^4.0.1","passport-local":"^1.0.0","reflect-metadata":"^0.2.2","rxjs":"^7.8.1","typeorm":"^0.3.28","typeorm-extension":"^3.9.0"},"devDependencies":{"@eslint/eslintrc":"^3.2.0","@eslint/js":"^9.18.0","@nestjs/cli":"^11.0.0","@nestjs/schematics":"^11.0.0","@nestjs/testing":"^11.0.1","@types/bcrypt":"^6.0.0","@types/express":"^5.0.0","@types/jest":"^30.0.0","@types/node":"^22.10.7","@types/passport-jwt":"^4.0.1","@types/passport-local":"^1.0.38","@types/supertest":"^6.0.2","eslint":"^9.18.0","eslint-config-prettier":"^10.0.1","eslint-plugin-prettier":"^5.2.2","globals":"^16.0.0","jest":"^30.0.0","prettier":"^3.4.2","source-map-support":"^0.5.21","supertest":"^7.0.0","ts-jest":"^29.2.5","ts-loader":"^9.5.2","ts-node":"^10.9.2","tsconfig-paths":"^4.2.0","typescript":"^5.7.3","typescript-eslint":"^8.20.0"},"jest":{"moduleFileExtensions":["js","json","ts"],"rootDir":"src","testRegex":".*\\.spec\\.ts$","transform":{"^.+\\.(t|j)s$":"ts-jest"},"collectCoverageFrom":["**/*.(t|j)s"],"coverageDirectory":"../coverage","testEnvironment":"node"},"gitHead":"446b65aa0c45a9b4f83b183534a1e838a45d3a62","_id":"@bilikaz/nestjs-starter-boilerplate@1.0.0","_nodeVersion":"24.14.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-k2pDC+t/KdfvaeSkeEWYC5DakpEWkiIkQxeXfgBgaIUPNKYmqgpGbSfehQMHTOhOwmx+SNxA7IFHiqnB5UWyAg==","shasum":"5038ec17b14dfb2d1a41967f3e43b697be69f42c","tarball":"https://registry.npmjs.org/@bilikaz/nestjs-starter-boilerplate/-/nestjs-starter-boilerplate-1.0.0.tgz","fileCount":70,"unpackedSize":104920,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCClnn68gvEprUHyXaf9a/VyLjQeEO6XBvvd2YsHIPulQIhANLLMNC7rBqzSNciDbbaR8zeGs0YfYQ82nJOhHK+IaYa"}]},"_npmUser":{"name":"bilikaz","email":"valdas@vbtech.eu"},"directories":{},"maintainers":[{"name":"bilikaz","email":"valdas@vbtech.eu"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nestjs-starter-boilerplate_1.0.0_1774380194064_0.28015783380332926"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-24T19:23:14.005Z","1.0.0":"2026-03-24T19:23:14.293Z","modified":"2026-03-24T19:23:14.526Z"},"maintainers":[{"name":"bilikaz","email":"valdas@vbtech.eu"}],"description":"Minimal NestJS boilerplate with TypeORM, JWT/OpenID authentication, and OpenAPI (Swagger) documentation out of the box","author":{"name":"bilikaz"},"license":"MIT","readme":"# NestJS Starter\n\nMinimal [NestJS](https://nestjs.com) boilerplate with TypeORM, JWT/OpenID authentication, and OpenAPI (Swagger) documentation out of the box.\n\n## Stack\n\n- **NestJS v11** — modular, decorator-based Node.js framework\n- **TypeORM 0.3** — ORM with MySQL driver, snake_case naming strategy, migration support\n- **Authentication** — JWT access/refresh tokens, local (username+password) strategy via Passport; roles-based access control (`@Roles`, `@Public`, `@OrganizationRoles` decorators) with composable OR-guard support via `AnyGuard`\n- **OpenAPI** — Swagger UI auto-generated from decorators, available at `/api`\n\n## Getting started\n\n```bash\nnpm install\ncp demo.env .env   # fill in real values\nnpm run start:dev\n```\n\n## Environment variables\n\nCopy `demo.env` to `.env` and adjust the values.\n\n| Variable | Description | Default |\n|---|---|---|\n| `APP_PORT` | HTTP port the server listens on | `3000` |\n| `APP_TITLE` | Swagger UI title | `Example` |\n| `APP_DESCRIPTION` | Swagger UI description | `description` |\n| `APP_VERSION` | Swagger UI version | `1.0` |\n| `DATABASE_HOST` | MySQL host | — |\n| `DATABASE_PORT` | MySQL port | — |\n| `DATABASE_USER` | MySQL username | — |\n| `DATABASE_PASSWORD` | MySQL password | — |\n| `DATABASE_NAME` | MySQL database name | — |\n| `JWT_SECRET` | Secret used to sign JWT tokens | — |\n| `JWT_EXPIRATION` | Access token lifetime in seconds | `900` |\n| `JWT_REFRESH_EXPIRATION` | Refresh token lifetime in seconds | `604800` |\n| `OIDC_ISSUER` | JWT `iss` claim value | — |\n| `OIDC_AUDIENCE` | JWT `aud` claim value | `api` |\n\n## Scripts\n\n```bash\nnpm run start:dev       # development with watch mode\nnpm run start:debug     # debug mode with Node inspector\nnpm run build           # compile TypeScript to dist/\nnpm run start:prod      # run compiled build\n\nnpm test                # unit tests\nnpm run test:watch      # unit tests in watch mode\nnpm run test:cov        # unit tests with coverage report\nnpm run test:e2e        # end-to-end tests\n\nnpm run lint            # ESLint with auto-fix\nnpm run format          # Prettier formatting\n\nnpm run seed:create -- -n db/seeds/DescriptiveName  # create a new seed file\nnpm run seed:run        # run all seeds\n\nnpm run migration:generate -- db/migrations/DescriptiveName  # generate migration from entity changes\nnpm run migration:run    # apply pending migrations\nnpm run migration:revert # revert last migration\nnpm run migration:show   # show migration status\n```\n\n## Database migrations\n\nMigrations live in `db/migrations/`. The datasource is configured in `db/datasource.ts`. Auto-synchronize is **disabled** — all schema changes must go through migrations.\n\n```bash\n# Generate a new migration from entity changes\nnpm run migration:generate -- db/migrations/DescriptiveName\n\n# Apply all pending migrations\nnpm run migration:run\n\n# Revert the last applied migration\nnpm run migration:revert\n\n# Show migration status (applied / pending)\nnpm run migration:show\n```\n\n> The migration commands use `typeorm-ts-node-commonjs` and load `db/datasource.ts` directly, so they pick up the `.env` file automatically.\n\n## Database seeding\n\nSeeding uses [typeorm-extension](https://github.com/tada5hi/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.\n\n```bash\n# Create a new seed file\nnpm run seed:create -- -n db/seeds/DescriptiveName\n\n# Run all seeds\nnpm run seed:run\n```\n\nThe initial seed creates an admin user:\n\n| Field | Value |\n|---|---|\n| username | `admin` |\n| email | `admin@localhost` |\n| password | `admin` |\n| role | `admin` |\n\n## Claude Code\n\nThis project includes a [Claude Code](https://claude.ai/code) skill for scaffolding new CRUD modules. With [Claude Code CLI](https://claude.ai/code) installed, run:\n\n```\n/createEntity <EntityName> <field:type[:options]> ...\n```\n\nThis 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:\n\n```bash\n# All actions require ADMIN role (default)\n/createEntity Product name:string price:decimal\n\n# Custom roles per action\n/createEntity Product name:string --create-roles=ADMIN --list-roles=USER,ADMIN --get-public\n\n# All actions public (no auth required)\n/createEntity Product name:string --no-roles\n```\n\nSee `.claude/skills/createEntity/SKILL.md` for the full field syntax and all available role flags.\n\nTo scaffold a new guard that integrates with the `AnyGuard` OR-composition system, use:\n\n```\n/createGuard <GuardName> <description of what it restricts>\n```\n\nThis 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.\n\n```bash\n# Guard that checks a user's subscription tier\n/createGuard SubscriptionTier restricts routes to users with a minimum subscription tier\n\n# Same, but don't add it to the global AnyGuard automatically\n/createGuard SubscriptionTier restricts routes to users with a minimum subscription tier --no-any-guard\n```\n\nSee `.claude/skills/createGuard/SKILL.md` for full details and the `AnyGuard` protocol contract.\n\n## Authorization\n\n### Global role guard — `@Roles`\n\nAll routes are protected by default. Use `@Roles(UserRole.X)` to restrict a route to specific system-level roles, or `@Public()` to opt out entirely.\n\n```ts\n@Roles(UserRole.ADMIN)   // only admins\n@Roles(UserRole.USER, UserRole.ADMIN)  // either role\n@Public()                // no auth required\n```\n\n### Organization-scoped guard — `@OrganizationRoles`\n\n`OrganizationRoleGuard` enforces membership and optional role within an organization. It reads `organizationId` from the route params (configurable via the decorator's `param` option).\n\n```ts\n@OrganizationRoles()                             // any member\n@OrganizationRoles(OrganizationUserRole.OWNER)   // owner only\n@OrganizationRoles(OrganizationUserRole.OWNER, OrganizationUserRole.MANAGER)\n\n// Custom param name (default is 'organizationId')\n@OrganizationRoles(OrganizationUserRole.OWNER, { param: 'orgId' })\n```\n\n### Composable OR logic — `AnyGuard`\n\nBy 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`:\n\n```ts\n// app.module.ts\n{ provide: APP_GUARD, useClass: AnyGuard(RoleGuard, OrganizationRoleGuard) }\n```\n\nThis means a route annotated with both decorators is accessible to system admins **or** organization owners — whichever check passes first:\n\n```ts\n@Roles(UserRole.ADMIN)\n@OrganizationRoles(OrganizationUserRole.OWNER)\n@Get(':organizationId/settings')\ngetSettings() { ... }\n```\n\n**How guards signal their result to `AnyGuard`** — each compatible guard exposes an `anyGuard: boolean` property:\n\n| `anyGuard` value | No decorator on route | Check passes | Check fails |\n|---|---|---|---|\n| `false` (standalone default) | `true` — transparent | `true` | `false` |\n| `true` (set by `AnyGuard`) | `false` — not applicable | `true` | throws `ForbiddenException` |\n\n`AnyGuard` resolves as follows:\n- Any guard returns `true` → **allowed**\n- All guards returned `false` (none applicable) → **allowed** (route has no restriction)\n- At least one threw and none returned `true` → **forbidden**\n\n### Adding a custom guard compatible with `AnyGuard`\n\nAny guard can participate in OR composition by following the same contract as `OrganizationRoleGuard`:\n\n1. Add a public `anyGuard = false` property.\n2. When `anyGuard` is `true`: return `false` if your decorator is absent, throw `ForbiddenException` if the check fails.\n3. Pass the class to `AnyGuard(...)` in `app.module.ts`.\n\n## Swagger / OpenAPI\n\nOnce the app is running, the interactive API documentation is available at:\n\n```\nhttp://localhost:<APP_PORT>/api\n```\n\nAll 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`.\n","readmeFilename":"README.md","_rev":"1-cff51877a3ac7fbcb91597350ff12c0b"}