scaffolding-nestjs-app

Creates a new NestJS app with logging, error handling, and all the right setup conventions.

Installation
Run `npx skills add "https://github.com/satanlittlehelper/ai-dev-kit" --skill "scaffolding-nestjs-app"` to install this skill, then follow its SKILL.md instructions for my next request.

Paste this into Claude Code, Cursor, or any agent that can run commands.

SKILL.mdShow the author's original SKILL.md
---
name: scaffolding-nestjs-app
description: Use when the user asks to create, scaffold, bootstrap, or add a new NestJS application or service — standalone or as a new app inside an Nx monorepo — including requests phrased as "make/create a new Nest app/service". Applies whenever the new app needs pino logging, a global exception filter + unified API response interceptor, this author's ESLint/Vitest/pre-commit/pre-push conventions, and baseline Dockerfile/docker-compose/Makefile/.env.example deployment files wired up.
---

# Scaffolding a new NestJS app

## Overview

Turns a blank directory (or a new app inside an existing Nx monorepo) into a NestJS service with the author's
standard baseline: pino logging, unified `ApiResponse` contract + global exception filter, ESLint/Prettier, Vitest
with coverage, husky pre-commit/pre-push, and Docker/Compose/Makefile deployment scaffolding. Templates live in
`templates/` next to this file — copy and adapt them, don't retype from memory.

## When to use

- User asks to create a new NestJS app/service/microservice from scratch.
- User asks to add a new backend app to an existing Nx (or other) monorepo.
- NOT for adding a single feature/module to an already-scaffolded app — this skill is for the initial bootstrap only.

## Step 0 — Detect the environment

```dot
digraph detect_env {
    "nx.json exists in target repo root?" [shape=diamond];
    "Any existing app/project in the repo?" [shape=diamond];
    "Use Nx generator" [shape=box];
    "Ask user: fresh Nx workspace or plain Nest CLI app?" [shape=diamond];
    "Use Nest CLI (nest new / nest generate)" [shape=box];
    "npx create-nx-workspace, then Nx generator" [shape=box];

    "nx.json exists in target repo root?" -> "Use Nx generator" [label="yes"];
    "nx.json exists in target repo root?" -> "Any existing app/project in the repo?" [label="no"];
    "Any existing app/project in the repo?" -> "Use Nest CLI (nest new / nest generate)" [label="yes"];
    "Any existing app/project in the repo?" -> "Ask user: fresh Nx workspace or plain Nest CLI app?" [label="no"];
    "Ask user: fresh Nx workspace or plain Nest CLI app?" -> "npx create-nx-workspace, then Nx generator" [label="Nx"];
    "Ask user: fresh Nx workspace or plain Nest CLI app?" -> "Use Nest CLI (nest new / nest generate)" [label="plain Nest"];
}
```

- **`nx.json` present** → `npx nx g @nx/nest:application apps/<app-name>` (ask for the app name/path if not given;
  add `@nx/nest` to devDependencies first if the plugin isn't installed yet).
- **No `nx.json`, but the repo already has at least one app/package** → this is a non-Nx multi-app repo; use
  `nest new <app-name>` or `nest generate` following whatever structure the existing apps use. Do not silently
  introduce Nx into a repo that doesn't use it.
- **No `nx.json` and no existing app at all (empty/fresh repo)** → this is the "first app in the project" case.
  **Always ask the user** whether to set up an Nx workspace (`npx create-nx-workspace@latest`, then the Nx generator
  above) or a plain standalone Nest CLI app (`npx @nestjs/cli new .`). Do not assume either — this decision shapes
  every later step (which template variants to use: `*.standalone.*` vs the Nx/monorepo ones).

## Step 1 — Install dependencies

Full list this skill needs — install latest versions (`@latest`), don't pin manually. In a monorepo, check the
workspace root `package.json` first and only add what's actually missing; most of the eslint/vitest tooling is
usually already there.

**Dependencies:**
```
npm install nestjs-pino pino pino-http class-validator class-transformer
```
Add `axios` too if the exception filter's axios-error branch will be used (see Step 5) — skip it otherwise.

**Dev dependencies:**
```
npm install -D pino-pretty eslint prettier eslint-config-prettier eslint-plugin-import eslint-plugin-prettier typescript-eslint vitest @vitest/coverage-v8 unplugin-swc vite-tsconfig-paths husky lint-staged
```
Monorepo only — also add `@nx/eslint-plugin` if it isn't already a workspace dependency.

## Step 2 — Post-generation cleanup: strict TypeScript, remove Jest and e2e scaffolding

**Strict mode is the default for every app in this stack — verify it, don't assume the generator set it.** Open the
app's `tsconfig.json` (standalone) or the workspace `tsconfig.base.json` (monorepo) and confirm
`"compilerOptions"."strict": true` is set. `nest new`'s default template already sets it, but `@nx/nest:application`
inherits from the workspace `tsconfig.base.json`, which may predate this convention — set it explicitly if missing.
Do not turn off individual strict sub-flags (`strictNullChecks`, `noImplicitAny`, etc.) to work around type errors
during scaffolding — fix the underlying type instead.

Both `nest new` and `@nx/nest:application` scaffold Jest by default — this author's stack uses **Vitest only**, and
no separate e2e test target. Immediately after generating the app, before touching anything else, delete:

- `jest.config.ts`/`jest.config.js` (app-level and, for a fresh standalone repo, the root one)
- `test/` (the generated e2e folder, typically containing `app.e2e-spec.ts` and `jest-e2e.json`)
- Any `*.spec.ts` files Jest-generated alongside sources (e.g. `src/app.controller.spec.ts`) — delete rather than
  port to Vitest; write real spec files later, per the project's own testing conventions, when there's actual logic
  to cover.
- `jest`-related devDependencies: `jest`, `@nestjs/testing` (only if not otherwise needed — this project doesn't use
  it), `ts-jest`, `@types/jest`, `jest-environment-node` (check `package.json` for exactly which ones the generator
  added; the Nx/Nest generator version determines the precise list — remove whatever it inserted).
- `jest` config blocks embedded in `package.json` (Nx's `@nx/nest:application` and plain `nest new` both add a
  top-level `"jest"` key) and any `test`/`test:e2e`/`test:watch`/`test:cov` npm scripts pointing at Jest — these get
  replaced by Vitest equivalents in Step 7.
- For an Nx monorepo app: also delete the generated `project.json` `test`/`e2e` targets that reference
  `@nx/jest:jest` — Step 8 replaces them with `nx:run-commands` + `vitest run`, matching the rest of the workspace's
  apps.

**Vitest/`tsc` wiring, so spec files don't break the production build or the IDE:**

1. Add `"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]` to `tsconfig.app.json` (next to `"include"`) — otherwise the production build picks up spec files and fails on Vitest-only globals like `vi`.
2. Create `tsconfig.spec.json`: `{ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vitest/globals"] }, "include": ["src/**/*.spec.ts", "src/**/*.test.ts", "vitest.config.ts"] }` — without it the IDE and `tsc` report `Cannot find name 'vi'` in spec files even though Vitest itself runs them fine.
3. In every spec file, import `vi` explicitly from `'vitest'` rather than relying on the global alone — the IDE resolves types from `tsconfig.spec.json` only when it's actually using that config, and an explicit import works regardless.

## App Baseline: Config, Global Prefix, Validation

Before Step 3, make sure `main.ts`/`app.module.ts` have this baseline, regardless of what the generator produced: a global `ConfigModule` validating `PORT` (Joi or class-validator, `getOrThrow` at usage sites — never a bare `process.env.PORT`), `app.setGlobalPrefix('api')`, a global `ValidationPipe` with `transform: true` + `whitelist: true` + implicit conversion enabled, and `useContainer(app, { fallbackOnErrors: true })` so `class-validator`'s custom decorators can use Nest DI. If the app sits behind a Traefik (or equivalent) reverse proxy that strips a service-name prefix before forwarding, the app's own routes stay at plain `/api/...` — the proxy's route pattern (e.g. `/{service_name}/api` with a strip-prefix middleware) is the proxy's concern, not something the app needs to know about internally.

## Step 3 — Ask about API versioning

**Always ask the user** whether the new app needs URI API versioning before writing `main.ts` — don't default to
either answer. Some apps in this stack enable it (`ext-gate`), others don't (`bot-gateway`, `miniapp-gate`,
`bot-publisher`); it's a per-app decision, not a blanket convention.

If yes, add to `main.ts` (after `setGlobalPrefix`, before `useContainer`):
```ts
import { VersioningType } from '@nestjs/common';

app.enableVersioning({
  type: VersioningType.URI,
  defaultVersion: '1',
});
```
This makes routes resolve as `/api/v1/...`. If no, skip it — routes stay at `/api/...`.

## Step 4 — Wire the pino logger

Copy the whole `templates/pino/` directory into the new app — it's five files that compose together, not one
monolithic module:

| Template file | Role |
|---|---|
| `pino-config.types.ts` | `AppLoggerOptions`, `AppLoggerContextFilter`, `PinoConfig` + their `is*` type guards |
| `pino-config.constants.ts` | `REDACT_AUTH_HEADER_PATHS` (auth/token headers redacted by default), `PINO_CONFIG_FILENAME` |
| `pino-config.helpers.ts` | `buildPinoParams` — pure builder: pretty-transport, redact paths, context-blacklist hook |
| `pino-config.loader.ts` | `loadPinoConfig` — reads optional `pino.config.json` from cwd when `PINO_CONFIG_ENABLED=true` |
| `app-logger.module.ts` | `AppLoggerModule.forRoot`/`forRootAsync` — thin `nestjs-pino` wrapper composing the four above |

Placement:
- Monorepo with multiple Nest apps already sharing code → put the whole directory in a shared lib (e.g.
  `libs/nest-logger`) if one doesn't already exist; if it does, just depend on it — **do not duplicate**.
- Standalone app or first app in a monorepo → `src/common/logger/` inside the app itself, same five files.

**What each option controls** (`AppLoggerOptions`, consumed by `buildPinoParams`):
- `level` — pino log level (`'info'`, `'debug'`, …).
- `pretty` — dev-mode `pino-pretty` transport (`colorize: true`, `translateTime: 'SYS:standard'`). Off in prod —
  NDJSON goes to whatever log shipper is downstream, colorized/formatted output would just be noise there.
- `singleLine` — passed straight through to `pino-pretty`'s own `singleLine` option; `true` keeps one log line per
  request even with a large `req`/`res` payload, `false` pretty-prints multi-line for easier local reading of deeply
  nested objects.
- `pinoConfigEnabled` — when `true`, `loadPinoConfig` requires a `pino.config.json` in `process.cwd()` shaped
  `{ contextFilter?: { blacklist: string[] }, redactPaths?: string[] }` and throws a descriptive error if it's
  missing or malformed; when `false` (the default), no file is read and only the built-in redact paths apply. Use
  this escape hatch for noisy per-project cases (a health-check module's own `context` flooding dev logs, a field the
  built-in redact list doesn't cover) instead of hand-editing the shared lib per project.

**`LOG_LEVEL`/`LOG_PRETTY`/`LOG_PRETTY_SINGLE_LINE`/`PINO_CONFIG_ENABLED` — pick one Joi shape for all four and stay
consistent.** Two shapes both work; the failure mode is mixing them:
- **String shape** (`Joi.string().empty('').valid('true', 'false').default('true')`, compared as `config.get<string>('LOG_PRETTY') === 'true'`)
  — the shape this skill defaults to; needed if the same env value is ever read outside `ConfigService` (e.g. echoed
  into a shell script) where a native boolean can't survive serialization.
- **Boolean shape** (`Joi.boolean().default(false)`, read as `config.getOrThrow<boolean>('LOG_PRETTY')`) — simpler
  when nothing outside Nest's own `ConfigService` needs the raw value.

**Whichever shape you pick, apply it identically to all four keys.** Mixing them is the actual bug this warning
exists for: declaring `LOG_PRETTY: Joi.boolean()` while comparing `config.get<string>('LOG_PRETTY') === 'true'`
silently evaluates to `false` — no validation error, just wrong logs, because a real `boolean` is never `===` to the
string `'true'`. Match the schema's declared type to how the factory reads it, key by key.

String-shape example:
```ts
LOG_LEVEL: Joi.string().default('info'),
LOG_PRETTY: Joi.string().empty('').valid('true', 'false').default('true'),
LOG_PRETTY_SINGLE_LINE: Joi.string().empty('').valid('true', 'false').default('true'),
PINO_CONFIG_ENABLED: Joi.string().empty('').valid('true', 'false').default('false'),
```

Wire into `app.module.ts` (string-shape factory — swap `=== 'true'`/`config.get<string>` for `config.getOrThrow<boolean>` throughout if using the boolean shape instead):
```ts
AppLoggerModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService): AppLoggerOptions => ({
    level: config.getOrThrow<string>('LOG_LEVEL'),
    pretty: config.get<string>('LOG_PRETTY') === 'true',
    singleLine: config.get<string>('LOG_PRETTY_SINGLE_LINE') === 'true',
    pinoConfigEnabled: config.get<string>('PINO_CONFIG_ENABLED') === 'true',
  }),
}),
```
And in `main.ts`: `NestFactory.create(AppModule, { bufferLogs: true })` then `app.useLogger(app.get(Logger))`
(`Logger` from `nestjs-pino`).

**Env vars — update all three places** per this repo's own env-var convention (Joi schema above, root `.env.example`,
and — in a monorepo with a `devops/` deployment layer — `devops/.env.example` + the corresponding
`docker-compose.prod.yml` service env block): `LOG_LEVEL`, `LOG_PRETTY`, `LOG_PRETTY_SINGLE_LINE`,
`PINO_CONFIG_ENABLED`. Default `LOG_PRETTY_SINGLE_LINE=true` and `PINO_CONFIG_ENABLED=false` everywhere unless the
project has a specific reason to change either.

After wiring, **actually start the app** (`LOG_PRETTY=true` in `.env`) and confirm colorized single-line output
appears — not raw NDJSON — before moving on. A silently-false `pretty` flag is easy to miss otherwise.

## Step 5 — Wire exception filter + API response interceptor

**First, check for duplication** (this is the step most likely to be skipped): grep the target repo for an existing
`ExceptionFilter`/response-interceptor implementation before creating a new one.
- **Found one, repo is a monorepo** → extract it into a shared lib (mirror this skill's own history: see
  `@chatbot-platform/helpers/nest` as a worked example of exactly this extraction) and have both the existing and
  new app depend on the shared version. Do not leave two divergent copies.
- **Found one, repo is standalone** → reuse it as-is; do not create a second one.
- **None found** → copy `templates/response/api.types.ts`, `api-exception-filter.ts`, `api-response.interceptor.ts`.
  Fix the `../types/api.types` import path to match where you place the files. If the app has no reason to call
  other HTTP APIs via axios, drop the `isAxiosError` branch and the `axios` import from the filter.

Register in `app.module.ts` providers — **not** `main.ts` `useGlobalFilters`/`useGlobalInterceptors` — and put
`ApiResponseInterceptor` **last** among any other `APP_INTERCEPTOR` entries so it wraps their output:
```ts
providers: [
  { provide: APP_INTERCEPTOR, useClass: ApiResponseInterceptor },
  { provide: APP_FILTER, useClass: ApiExceptionFilter },
],
```

## Step 6 — Wire health endpoint

Every new app in this stack gets a simple, dependency-free liveness endpoint — no `@nestjs/terminus`, no DB/Redis
readiness checks unless the user explicitly asks for those. Pattern (inline, no template file to copy — small enough
to write directly):

`src/app/app.controller.ts`:
```ts
import { Controller, Get } from '@nestjs/common';
import { Public } from '<path-to-shared-public-decorator-or-local-equivalent>';

import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Public()
  @Get('health')
  getHealth(): { service: string; status: string } {
    return this.appService.getHealth();
  }
}
```

`src/app/app.service.ts` (or `src/app/app.service/app.service.ts` if the app already uses the dot-notation subfolder
convention for testable services):
```ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHealth(): { service: string; status: string } {
    return {
      service: '<app-name>',
      status: 'ok',
    };
  }
}
```

Register both in `app.module.ts`: `controllers: [AppController]`, add `AppService` to `providers`. The `Public`
decorator marks the route so any global auth guard (`APP_GUARD`) skips it — if the app's guard doesn't yet support a
public-route bypass via `Reflector`/metadata, add that bypass to the guard first (see the guard's own `canActivate`
for where to add a `Reflector.getAllAndOverride` check before the auth logic). Resulting path: `/api/health`, or
`/api/v1/health` if Step 3 (versioning) was enabled for this app.

## Step 7 — ESLint + Prettier

Copy `templates/eslint/eslint.base.config.mjs` to the repo root (or merge with the existing one if present — don't
overwrite a customized base). Strip the `@nx/enforce-module-boundaries` block and the `apps/*/libs/*` tsconfig glob
paths for a standalone (non-Nx) app — see the inline comments in the template for exactly what to remove.

For a monorepo app, also copy `templates/eslint/eslint.app.config.mjs`, replacing `__APP_NAME__` with the real app
name, as `apps/<app-name>/eslint.config.mjs`.

Copy `templates/eslint/.prettierrc` and `templates/eslint/.prettierignore` to the repo root (skip if both already
exist there — don't overwrite a customized config). `eslint.base.config.mjs` enforces `prettier/prettier: 'error'`,
so formatting mismatches between `.prettierrc` and the ESLint rule will fail lint otherwise.

## Step 8 — Vitest

Copy `templates/vitest/vitest.config.ts` and `templates/vitest/vitest.setup.ts` into the new app (`vitest.config.ts`
at the app root, `src/test/vitest.setup.ts`). Keep the `coverage.exclude` list — it encodes this author's convention
that `*.api.service.ts`, `*.repository.ts`, DTOs, modules, controllers, and the exception filter are not unit-tested
(logic-free glue or already covered indirectly). Leave `thresholds` commented out for a brand-new app with no tests
yet — uncomment once the project has meaningful coverage, don't block the first commits on an 80% threshold.

## Step 9 — Pre-commit / pre-push hooks

Run `npx husky init` if `.husky/` doesn't exist yet. Copy:
- `templates/husky/pre-commit` → `.husky/pre-commit` (same in both paths).
- Monorepo: `templates/husky/pre-push.nx` → `.husky/pre-push`, `templates/husky/lintstagedrc.nx.js` →
  `.lintstagedrc.js` (root, shared across apps — don't duplicate per-app).
- Standalone: `templates/husky/pre-push.standalone` → `.husky/pre-push`,
  `templates/husky/lintstagedrc.standalone.js` → `.lintstagedrc.js`.

Make both hook files executable (`chmod +x .husky/pre-commit .husky/pre-push`).

## Step 10 — Docker / docker-compose / Makefile / .env.example

- **Standalone app**: copy `templates/docker/Dockerfile.standalone` → `devops/Dockerfile`,
  `templates/docker/docker-compose.standalone.yml` → `devops/docker-compose.yml`,
  `templates/docker/Makefile.standalone` → `Makefile`, `templates/docker/.env.example.standalone` → `.env.example`.
- **Monorepo, shared parametrized Dockerfile already exists** (`ARG APP_NAME`) → read
  `templates/docker/dockerfile-monorepo-reference.md`, confirm the existing `devops/Dockerfile` matches that shape,
  and do **not** create a new one — just add a service block using
  `templates/docker/docker-compose-service-snippet.monorepo.md` as the pattern (replace `__APP_NAME__` /
  `__APP_NAME_UPPER__` placeholders), and add the new env vars per
  `templates/docker/env-example-snippet.monorepo.md`. Reuse the existing root `Makefile` unchanged.
- **Monorepo, first app, no devops/ setup yet** → create `devops/Dockerfile` from
  `templates/docker/dockerfile-monorepo-reference.md`'s reference block, and the root `Makefile` from
  `templates/docker/Makefile.monorepo-reference` (replace `__PROJECT_NAME__` with the repo name). That Makefile's
  `prod-*`/`dev0-*` targets reference `docker-compose.networks.brandnew.yml`/`.existing.yml` and a
  `docker-compose.base.yml` — these encode a specific brand-new-vs-existing-Docker-network deployment topology from
  the source project; only bring them in if the new repo needs that same split. Otherwise drop the `_BNN`/`_EXN`
  variables and point `COMPOSE_PROD` targets straight at `docker-compose.prod.yml`.

## Step 11 — Monorepo: add a root `package.json` start script

Monorepo only (skip for standalone apps — `npm run start` is already the app's own entrypoint there). Add a
`start:<app-name>` script to the workspace root `package.json`, matching the existing convention (e.g.
`"start:ext-gate": "nx run ext-gate:serve"`). Without it the new app has no documented local-run entrypoint
alongside the other apps' `start:*` scripts.

## Step 12 — Document the conventions in the target repo

Append `templates/docs/claude-md-nestjs-snippet.md` (with `__APP_NAME__` replaced and paths adjusted) into the
target repo's `CLAUDE.md` and/or `AGENTS.md`, next to whatever other conventions already exist there. If the repo
has neither file yet, create `CLAUDE.md` at the repo root with this content as a starting point.

Also append `templates/docs/claude-md-skills-workflow-snippet.md` — this is stack-agnostic (how to work with
skills/subagents, plan mode, commit discipline, workflow-artifact locations) and belongs in every project's
`CLAUDE.md`, not just NestJS ones. Skip it only if the target repo's `CLAUDE.md` already documents equivalent
skills-workflow conventions — don't duplicate.

## Step 13 — .gitignore AI artifacts

Append `templates/docs/gitignore-ai-artifacts-snippet.txt` to the target repo's `.gitignore` (create the file if it
doesn't exist). `CLAUDE.md`/`AGENTS.md`/`MEMORY.md` are written by Step 11 above but must stay untracked — they are
AI working artifacts, not project source. Check for existing entries first; don't duplicate lines already present.

## Quick reference — template files

| File | Purpose | Standalone-only edits |
|---|---|---|
| `templates/pino/app-logger.module.ts` | `nestjs-pino` wrapper module (composes the four files below) | none |
| `templates/pino/pino-config.types.ts` | `AppLoggerOptions`/`PinoConfig` types + type guards | none |
| `templates/pino/pino-config.constants.ts` | Default redact paths, `pino.config.json` filename | none |
| `templates/pino/pino-config.helpers.ts` | `buildPinoParams` pure builder | none |
| `templates/pino/pino-config.loader.ts` | Optional `pino.config.json` loader | none |
| `templates/response/api.types.ts` | `ApiResponse<T>` contract | none |
| `templates/response/api-exception-filter.ts` | Global `@Catch()` filter | drop axios branch if unused |
| `templates/response/api-response.interceptor.ts` | Wraps controller returns | none |
| `templates/eslint/eslint.base.config.mjs` | Root flat ESLint config | strip Nx boundary rule + glob tsconfig paths |
| `templates/eslint/eslint.app.config.mjs` | Per-app ESLint override | monorepo only |
| `templates/eslint/.prettierrc` + `.prettierignore` | Prettier formatting rules | none |
| `templates/vitest/vitest.config.ts` + `vitest.setup.ts` | Test runner + coverage | none |
| `templates/husky/pre-commit` | lint-staged hook | none |
| `templates/husky/pre-push.nx` / `pre-push.standalone` | test-on-push hook | pick one |
| `templates/husky/lintstagedrc.nx.js` / `lintstagedrc.standalone.js` | lint-staged config | pick one |
| `templates/docker/Dockerfile.standalone` / `dockerfile-monorepo-reference.md` | Multi-stage build | pick one |
| `templates/docker/docker-compose.standalone.yml` / `docker-compose-service-snippet.monorepo.md` | Deploy config | pick one |
| `templates/docker/Makefile.standalone` / `Makefile.monorepo-reference` | `local-up`/`down`/`clean` targets | pick one |
| `templates/docker/.env.example.standalone` / `env-example-snippet.monorepo.md` | Env var baseline | pick one |
| `templates/docs/claude-md-nestjs-snippet.md` | NestJS-specific conventions writeup for the target repo | adjust paths |
| `templates/docs/claude-md-skills-workflow-snippet.md` | Stack-agnostic skills/subagents workflow rules | none |
| `templates/docs/gitignore-ai-artifacts-snippet.txt` | Keeps CLAUDE.md/AGENTS.md/MEMORY.md untracked | none |

## Common mistakes

| Mistake | Fix |
|---|---|
| Registering the filter/interceptor via `app.useGlobalFilters()` in `main.ts` | Register via `APP_FILTER`/`APP_INTERCEPTOR` providers in `app.module.ts` instead |
| Putting `ApiResponseInterceptor` before other `APP_INTERCEPTOR`s | It must be last so it wraps their output |
| Creating a second exception filter/interceptor when one already exists in the monorepo | Extract the existing one into a shared lib and reuse |
| Copying the Nx `@nx/enforce-module-boundaries` ESLint rule into a standalone app | Strip it — there are no libs/apps boundaries to enforce |
| Setting hard coverage thresholds on day one | Leave `thresholds` commented out until the app has real tests |
| Creating a brand-new per-app Dockerfile in a monorepo that already has a shared parametrized one | Reuse the shared `devops/Dockerfile` with `ARG APP_NAME`, just add a compose service block |
| Adding a new env var to only one of the three required places | Joi schema + root `.env.example` + `devops/.env.example` (+ compose `environment:` block) — all three, every time |
| Skipping the health endpoint because the app "is internal" | Every HTTP app gets `GET /health` (or `/api/health` with global prefix) — internal-only apps still need it for orchestration liveness checks |

Ships with 30 supporting files:

  • templates/docker/.env.example.standalone
  • templates/docker/docker-compose-service-snippet.monorepo.md
  • templates/docker/docker-compose.standalone.yml
  • templates/docker/dockerfile-monorepo-reference.md
  • templates/docker/Dockerfile.standalone
  • templates/docker/env-example-snippet.monorepo.md
  • templates/docker/Makefile.monorepo-reference
  • templates/docker/Makefile.standalone
  • templates/docs/claude-md-nestjs-snippet.md
  • templates/docs/claude-md-skills-workflow-snippet.md
  • templates/docs/gitignore-ai-artifacts-snippet.txt
  • templates/eslint/.prettierignore
  • templates/eslint/.prettierrc
  • templates/eslint/eslint.app.config.mjs
  • templates/eslint/eslint.base.config.mjs
  • templates/husky/lintstagedrc.nx.js
  • templates/husky/lintstagedrc.standalone.js
  • templates/husky/pre-commit
  • templates/husky/pre-push.nx
  • templates/husky/pre-push.standalone
  • … and 10 more

Mirrored from the author's public source. Install counts from the open skills registry.

The systems behind these skills get built for partners every week.

Partner with us