https://github.com/hngprojects/backend-nestjs-starter
https://github.com/hngprojects/backend-nestjs-starter
Last synced: 7 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/hngprojects/backend-nestjs-starter
- Owner: hngprojects
- Created: 2026-05-12T21:47:36.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-29T03:16:00.000Z (18 days ago)
- Last Synced: 2026-05-29T05:12:35.533Z (18 days ago)
- Language: TypeScript
- Size: 137 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# NestJS Starter
A 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.
## Stack
- **Runtime**: NestJS 11 + TypeScript 5
- **Database**: PostgreSQL via TypeORM (accessed through `@hng-sdk/orm`'s `AbstractModelAction` repository pattern)
- **Auth**: JWT access + refresh tokens (`@nestjs/jwt` + Passport)
- **Validation**: `class-validator` + `class-transformer` for HTTP DTOs
- **Env validation**: [`@t3-oss/env-core`](https://env.t3.gg) + Zod (fail-fast on missing/invalid env vars)
- **Docs**: Swagger at `/docs`
- **Hardening**: Helmet, compression, CORS, global exception filter, response envelope
## Prerequisites
- Node.js 20+
- pnpm (or npm/yarn — adjust commands accordingly)
- A running PostgreSQL 14+ instance
## Quick start
```bash
# 1. Install
pnpm install
# 2. Configure
cp .env.example .env
# edit .env with your DB credentials and at least 32-char JWT secrets
# 3. Create the database (one-time)
createdb nestjs_starter # or your preferred client
# 4. Apply migrations
pnpm migration:run
# 5. (optional) Seed an admin user
pnpm seed
# creates admin@example.com / Admin@123456
# 6. Run
pnpm start:dev
```
Open `http://localhost:3000/docs` for the Swagger UI.
## Scripts
### App
| Script | Purpose |
|---|---|
| `pnpm start:dev` | Run with watch mode |
| `pnpm start:debug` | Run with `--inspect` debugger |
| `pnpm start:prod` | Run the compiled `dist/main.js` |
| `pnpm build` | Compile to `dist/` |
| `pnpm lint` | Lint and auto-fix |
| `pnpm format` | Prettier |
| `pnpm test` | Unit tests |
| `pnpm test:e2e` | End-to-end tests |
| `pnpm test:cov` | Coverage report |
### Database
| Script | Purpose |
|---|---|
| `pnpm migration:run` | Apply all pending migrations |
| `pnpm migration:revert` | Revert the most recent migration |
| `pnpm migration:show` | List migrations and their status |
| `pnpm migration:generate src/database/migrations/` | Diff entities vs DB and generate a migration |
| `pnpm migration:create src/database/migrations/` | Create an empty migration |
| `pnpm schema:drop` | Drop all tables (destructive — dev only) |
| `pnpm seed` | Run all seeders |
| `pnpm db:reset` | Drop schema, run migrations, run seeders |
> The `migration:generate` script requires a live database connection so TypeORM can diff against the current schema.
## Folder structure
```
src/
├── common/ # cross-cutting: decorators, filters, interceptors
│ ├── decorators/ # @Public(), @CurrentUser()
│ ├── filters/ # global HttpExceptionFilter
│ └── interceptors/ # logging + response envelope
├── config/ # env (t3-env), app/database/jwt config
├── database/
│ ├── data-source.ts # TypeORM CLI DataSource
│ ├── migrations/
│ └── seeds/
├── modules/
│ ├── auth/ # /auth/register, /login, /refresh, /logout, /me
│ ├── health/ # /health (public)
│ └── users/ # CRUD example using the repository pattern
│ ├── actions/ # UserModelAction extends AbstractModelAction
│ ├── dto/
│ └── entities/
├── app.module.ts
└── main.ts
```
## Architecture
### Repository pattern via `@hng-sdk/orm`
Services never depend on TypeORM `Repository` directly. Instead, each entity gets a `*ModelAction` class that extends `AbstractModelAction` and exposes a uniform CRUD API (`create`, `get`, `find`, `list`, `update`, `delete`, `save`) plus any domain-specific helpers.
```ts
// modules/users/actions/user.action.ts
@Injectable()
export class UserModelAction extends AbstractModelAction {
constructor(@InjectRepository(User) repository: Repository) {
super(repository, User);
}
findByEmail(email: string) {
return this.get({ identifierOptions: { email } });
}
}
```
```ts
// modules/users/users.service.ts
@Injectable()
export class UsersService {
constructor(private readonly userModelAction: UserModelAction) {}
findOne(id: string) {
return this.userModelAction.get({ identifierOptions: { id } });
}
}
```
### Adding a new module
1. Create `src/modules//`
2. Define the entity in `entities/.entity.ts`
3. Create the model action in `actions/.action.ts`
4. Implement service and controller
5. Wire up the module: `imports: [TypeOrmModule.forFeature([Entity])]`, providers include the model action
6. Register the module in `AppModule.imports`
7. Generate a migration: `pnpm migration:generate src/database/migrations/Add`
8. Apply it: `pnpm migration:run`
### Env validation
`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`:
```ts
import { env } from './config/env';
const port = env.PORT; // typed as number
```
### Auth flow
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
| `/auth/register` | POST | public | Create account, returns access + refresh tokens |
| `/auth/login` | POST | public | Returns access + refresh tokens |
| `/auth/refresh` | POST | public | Issue a new access token from a refresh token |
| `/auth/logout` | POST | bearer | Revoke the current refresh token |
| `/auth/me` | GET | bearer | Return current user |
The global `JwtAuthGuard` protects every route by default. Decorate handlers (or controllers) with `@Public()` to opt out.
### Response envelope
`TransformInterceptor` wraps successful responses:
```json
{
"success": true,
"data": { ... }
}
```
For paginated responses, `paginationMeta` from `@hng-sdk/orm` is hoisted into `meta`.
Errors go through `HttpExceptionFilter`:
```json
{
"success": false,
"statusCode": 400,
"error": "BadRequestException",
"message": ["email must be an email"],
"path": "/api/users",
"timestamp": "2026-04-28T12:34:56.000Z"
}
```
## Environment variables
See `.env.example` for the full list. Critical ones:
| Variable | Notes |
|---|---|
| `DATABASE_*` | host/port/user/password/name |
| `DATABASE_SYNC` | **Always `false` in non-dev** — use migrations |
| `DATABASE_SSL` | `true` for managed providers (Neon, Supabase, RDS) |
| `JWT_ACCESS_SECRET` | Min 32 chars |
| `JWT_REFRESH_SECRET` | Min 32 chars, must differ from access secret |
| `SWAGGER_ENABLED` | Set to `false` in production if you don't want public docs |
## License
UNLICENSED