{"id":50695697,"url":"https://github.com/hngprojects/skill-bridge-api","last_synced_at":"2026-06-09T06:08:10.696Z","repository":{"id":357718679,"uuid":"1238229057","full_name":"hngprojects/skill-bridge-api","owner":"hngprojects","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-04T20:41:02.000Z","size":1732,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"dev","last_synced_at":"2026-06-04T22:14:20.209Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hngprojects.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-13T23:56:40.000Z","updated_at":"2026-06-04T20:41:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hngprojects/skill-bridge-api","commit_stats":null,"previous_names":["hngprojects/skill-bridge-api"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hngprojects/skill-bridge-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fskill-bridge-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fskill-bridge-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fskill-bridge-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fskill-bridge-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hngprojects","download_url":"https://codeload.github.com/hngprojects/skill-bridge-api/tar.gz/refs/heads/dev","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hngprojects%2Fskill-bridge-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34093840,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-09T06:08:09.982Z","updated_at":"2026-06-09T06:08:10.685Z","avatar_url":"https://github.com/hngprojects.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NestJS Starter\n\nA production-ready NestJS 11 starter with PostgreSQL, JWT auth, the repository pattern via [`@hng-sdk/orm`](https://www.npmjs.com/package/@hng-sdk/orm), and migrations out of the box.\n\n## Stack\n\n- **Runtime**: NestJS 11 + TypeScript 5\n- **Database**: PostgreSQL via TypeORM (accessed through `@hng-sdk/orm`'s `AbstractModelAction` repository pattern)\n- **Auth**: JWT access + refresh tokens (`@nestjs/jwt` + Passport)\n- **Validation**: `class-validator` + `class-transformer` for HTTP DTOs\n- **Env validation**: [`@t3-oss/env-core`](https://env.t3.gg) + Zod (fail-fast on missing/invalid env vars)\n- **Docs**: Swagger at `/docs`\n- **Hardening**: Helmet, compression, CORS, global exception filter, response envelope\n\n## Prerequisites\n\n- Node.js 20+\n- pnpm (or npm/yarn — adjust commands accordingly)\n- A running PostgreSQL 14+ instance\n\n## Quick start\n\n```bash\n# 1. Install\npnpm install\n\n# 2. Configure\ncp .env.example .env\n# edit .env with your DB credentials and at least 32-char JWT secrets\n\n# 3. Create the database (one-time)\ncreatedb nestjs_starter   # or your preferred client\n\n# 4. Apply migrations\npnpm migration:run\n\n# 5. (optional) Seed an admin user\npnpm seed\n# creates admin@example.com / Admin@123456\n\n# 6. Run\npnpm start:dev\n```\n\nOpen `http://localhost:3000/docs` for the Swagger UI.\n\n## Scripts\n\n### App\n| Script | Purpose |\n|---|---|\n| `pnpm start:dev` | Run with watch mode |\n| `pnpm start:debug` | Run with `--inspect` debugger |\n| `pnpm start:prod` | Run the compiled `dist/main.js` |\n| `pnpm build` | Compile to `dist/` |\n| `pnpm lint` | Lint and auto-fix |\n| `pnpm format` | Prettier |\n| `pnpm test` | Unit tests |\n| `pnpm test:e2e` | End-to-end tests |\n| `pnpm test:cov` | Coverage report |\n\n### Database\n| Script | Purpose |\n|---|---|\n| `pnpm migration:run` | Apply all pending migrations |\n| `pnpm migration:revert` | Revert the most recent migration |\n| `pnpm migration:show` | List migrations and their status |\n| `pnpm migration:generate src/database/migrations/\u003cName\u003e` | Diff entities vs DB and generate a migration |\n| `pnpm migration:create src/database/migrations/\u003cName\u003e` | Create an empty migration |\n| `pnpm schema:drop` | Drop all tables (destructive — dev only) |\n| `pnpm seed` | Run all seeders |\n| `pnpm db:reset` | Drop schema, run migrations, run seeders |\n\n### Question bank\n| Script | Purpose |\n|---|---|\n| `pnpm generate:question-bank` | Run the scheduled question-bank generator |\n\nExample dry run:\n\n```bash\npnpm generate:question-bank -- --dry-run --batch-size 5\n```\n\nFor PM2, use `dist/scripts/generate-question-bank.js` as a one-shot app and trigger it from host cron with `pm2 startOrReload ecosystem.config.cjs --only question-generator --env production --update-env`.\n\n\u003e The `migration:generate` script requires a live database connection so TypeORM can diff against the current schema.\n\n## Folder structure\n\n```\nsrc/\n├── common/                 # cross-cutting: decorators, filters, interceptors\n│   ├── decorators/         # @Public(), @CurrentUser()\n│   ├── filters/            # global HttpExceptionFilter\n│   └── interceptors/       # logging + response envelope\n├── config/                 # env (t3-env), app/database/jwt config\n├── database/\n│   ├── data-source.ts      # TypeORM CLI DataSource\n│   ├── migrations/\n│   └── seeds/\n├── modules/\n│   ├── auth/               # /auth/register, /login, /refresh, /logout, /me\n│   ├── health/             # /health (public)\n│   └── users/              # CRUD example using the repository pattern\n│       ├── actions/        # UserModelAction extends AbstractModelAction\u003cUser\u003e\n│       ├── dto/\n│       └── entities/\n├── app.module.ts\n└── main.ts\n```\n\n## Architecture\n\n### Repository pattern via `@hng-sdk/orm`\n\nServices never depend on TypeORM `Repository\u003cT\u003e` directly. Instead, each entity gets a `*ModelAction` class that extends `AbstractModelAction\u003cT\u003e` and exposes a uniform CRUD API (`create`, `get`, `find`, `list`, `update`, `delete`, `save`) plus any domain-specific helpers.\n\n```ts\n// modules/users/actions/user.action.ts\n@Injectable()\nexport class UserModelAction extends AbstractModelAction\u003cUser\u003e {\n  constructor(@InjectRepository(User) repository: Repository\u003cUser\u003e) {\n    super(repository, User);\n  }\n\n  findByEmail(email: string) {\n    return this.get({ identifierOptions: { email } });\n  }\n}\n```\n\n```ts\n// modules/users/users.service.ts\n@Injectable()\nexport class UsersService {\n  constructor(private readonly userModelAction: UserModelAction) {}\n\n  findOne(id: string) {\n    return this.userModelAction.get({ identifierOptions: { id } });\n  }\n}\n```\n\n### Adding a new module\n\n1. Create `src/modules/\u003cname\u003e/`\n2. Define the entity in `entities/\u003cname\u003e.entity.ts`\n3. Create the model action in `actions/\u003cname\u003e.action.ts`\n4. Implement service and controller\n5. Wire up the module: `imports: [TypeOrmModule.forFeature([Entity])]`, providers include the model action\n6. Register the module in `AppModule.imports`\n7. Generate a migration: `pnpm migration:generate src/database/migrations/Add\u003cName\u003e`\n8. Apply it: `pnpm migration:run`\n\n### Env validation\n\n`src/config/env.ts` uses `@t3-oss/env-core` with Zod. The app fails to boot with a readable error if any required variable is missing or invalid. Import the typed `env` object instead of reaching into `process.env`:\n\n```ts\nimport { env } from './config/env';\nconst port = env.PORT; // typed as number\n```\n\n### Auth flow\n\n| Endpoint | Method | Auth | Purpose |\n|---|---|---|---|\n| `/auth/register` | POST | public | Create account, returns access + refresh tokens |\n| `/auth/login` | POST | public | Returns access + refresh tokens |\n| `/auth/refresh` | POST | public | Issue a new access token from a refresh token |\n| `/auth/logout` | POST | bearer | Revoke the current refresh token |\n| `/auth/me` | GET | bearer | Return current user |\n\nThe global `JwtAuthGuard` protects every route by default. Decorate handlers (or controllers) with `@Public()` to opt out.\n\n### Response envelope\n\n`TransformInterceptor` wraps successful responses:\n\n```json\n{\n  \"success\": true,\n  \"data\": { ... }\n}\n```\n\nFor paginated responses, `paginationMeta` from `@hng-sdk/orm` is hoisted into `meta`.\n\nErrors go through `HttpExceptionFilter`:\n\n```json\n{\n  \"success\": false,\n  \"statusCode\": 400,\n  \"error\": \"BadRequestException\",\n  \"message\": [\"email must be an email\"],\n  \"path\": \"/api/users\",\n  \"timestamp\": \"2026-04-28T12:34:56.000Z\"\n}\n```\n\n## Environment variables\n\nSee `.env.example` for the full list. Critical ones:\n\n| Variable | Notes |\n|---|---|\n| `DATABASE_*` | host/port/user/password/name |\n| `DATABASE_SYNC` | **Always `false` in non-dev** — use migrations |\n| `DATABASE_SSL` | `true` for managed providers (Neon, Supabase, RDS) |\n| `JWT_ACCESS_SECRET` | Min 32 chars |\n| `JWT_REFRESH_SECRET` | Min 32 chars, must differ from access secret |\n| `SWAGGER_ENABLED` | Set to `false` in production if you don't want public docs |\n\n## License\n\nUNLICENSED\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhngprojects%2Fskill-bridge-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhngprojects%2Fskill-bridge-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhngprojects%2Fskill-bridge-api/lists"}