Imported from JimmyPaolini/codebase (
tools/validation/AGENTS.md). Install upstream withnpx skills add JimmyPaolini/codebase --skill validation. Copyright stays with the author.
Validation: NestJS Command-Line Application
Quick Start
Type: Node.js CLI application (NestJS + nest-commander)
Purpose:
Run the repository's one-sided checks — the ones with a check and no write —
collecting every failure and printing the command that fixes each.
Run Locally
cp .env.default .env # Fill in required environment variables
nx run validation:start
What belongs here, and what deliberately does not
A check belongs here when it has no write side and when it already depends on
Node. A check with both sides belongs in
synchronization, whose SynchronizableCommand
contract is check/write reconciliation.
These five stay in scripts/git/ on purpose, and moving them here would be a
regression rather than finishing the job:
| Script | Runs from |
|---|---|
check-commit-signing-configuration.sh |
the pre-commit hook |
check-push-commit-signatures.sh |
the pre-push hook |
check-gh-authentication.sh |
a terminal, and the session hooks |
validate-session-commit-signing.sh |
a session start |
validate-session-gh-authentication.sh |
a session start |
Each uses only git, gpg, and gh. Bringing them into this CLI would make
committing and pushing depend on a healthy node_modules — exactly the state
someone is in when they are committing the fix for a broken one. The
discriminator is not startup cost: the CLI boots in about 0.83s and the shell
session hook takes about 0.77s, because the shell hook shells out to Node
anyway. It is whether the check still works when the install is broken.
Architecture Overview
Tech Stack
- Framework: NestJS (modules, dependency injection, providers)
- CLI runner:
nest-commander(CommandRunner+@Command()decorator) - Env validation:
@nestjs/config+zod(environmentSchemain.constants.ts) - Logging:
@codebase/logger— apino-backedLoggerService(Scope.TRANSIENT) - Language: Strict TypeScript
Execution Flow
src/main.ts
└─ CommandFactory.run(MainModule)
└─ domain command modules ← add under src/modules/
Directory Layout
src/
main.ts # Bootstrap — do not modify
main.module.ts # Root NestJS module (imports ConfigModule, LoggerModule)
constants.ts # Zod environmentSchema for env validation
modules/
<domain>/ # Add feature modules here
<domain>.module.ts
<domain>.command.ts
<domain>.service.ts
<domain>.types.ts
<domain>.constants.ts
<domain>.<tier>.test.ts
testing/ # Shared test utilities
Development
Adding Business Logic
- Add domain command modules — create
src/modules/<domain>/with a NestJS module, command, service, types, and constants. - Register in root module — import the new module in
main.module.ts. - Validate env vars — extend
environmentSchemainconstants.tswith all required environment variables.
Logging
LoggerService and LoggerModule come from @codebase/logger — this project does not define its own logger. Add "@codebase/logger": "workspace:*" to dependencies, then import LoggerModule once in the root module; it is @Global(), so feature modules inject LoggerService without importing it.
LoggerService is Scope.TRANSIENT — each injecting class gets its own instance. Always call setContext in the constructor:
constructor(private readonly logger: LoggerService) {
super();
this.logger.setContext(MyService.name);
}
Outputs structured JSON in production (NODE_ENV=production) and pretty-printed logs in development.
Key Commands
Always prefer running tasks through Nx rather than calling the underlying tools directly.
nx run validation:start # Run the command-line application
nx run validation:typecheck-code,lint-code,format-code,deprecate-code,guard-code # Every static check, in one graph
nx run validation:typecheck # tsc --noEmit
nx run validation:oxfmt # Formatting
Testing
Follow the codebase's strict three-tier testing strategy. Co-locate test files with the source they test.
nx run validation:vitest:unit # Fast (<100ms) — pure logic, mocked DI
nx run validation:vitest:integration # Moderate (1-2s) — real database/API I/O
nx run validation:vitest:end-to-end # Slow (30-60s) — full CLI execution
| Tier | File pattern | What to test |
|---|---|---|
| Unit | *.unit.test.ts |
Pure functions, service methods with mocked deps |
| Integration | *.integration.test.ts |
Database queries, external API clients |
| End-to-end | *.end-to-end.test.ts |
Full CommandFactory.run() execution |
See the testing-strategy skill and testing-mocks skill for patterns and mock conventions.
Writing Modules
Use the generator to scaffold new domain modules, then implement the service:
nx g conformetry:nestjs-service-module --name=<domain>
This creates five files in src/modules/<domain>/:
| File | Purpose |
|---|---|
<domain>.module.ts |
Declares providers, imports, and exports |
<domain>.service.ts |
Business logic — the only place you write domain code |
<domain>.constants.ts |
Regex, enums, static config — never inline magic values |
<domain>.types.ts |
TypeScript types scoped to this module |
<domain>.service.unit.test.ts |
Unit tests bootstrapped with Test.createTestingModule |
Module file
Register the service in both providers and exports so consumers can inject it:
@Module({
controllers: [],
exports: [MyDomainService],
imports: [TypeOrmModule.forFeature([MyEntity]), LoggerModule],
providers: [MyDomainService],
})
export class MyDomainModule {}
Add a JSDoc comment on the module class describing what domain it owns.
Service file
Follow the section-comment layout from the template — it keeps large services scannable:
@Injectable()
export class MyDomainService {
// 🏗 Dependency Injection
constructor(
@InjectRepository(MyEntity)
private readonly repo: Repository<MyEntity>,
private readonly logger: LoggerService,
) {
this.logger.setContext(MyDomainService.name);
}
// 🔐 Private Fields
// 🔑 Public Fields
// 🔏 Private Methods
// 🌎 Public Methods
}
Key rules:
- Call
setContextin every constructor — always useMyClass.name, never a string literal. - Inject
LoggerServiceas the last constructor parameter (after repository/domain deps). - Private first — keep internal helpers in the
🔏 Private Methodssection, expose only what callers need under🌎 Public Methods. readonlyeverything in the constructor — all injected deps must beprivate readonly.- One service per module — if a service grows too large, extract a sub-domain into its own module.
Constants file
Move all inline values to .constants.ts to keep services readable:
// ♟️ Constants
export const MY_SKIP_REGEX = /(alternative)|(archaic)|(synonym)/i;
export const DEFAULT_PAGE_SIZE = 100;
Types file
Put all module-local TypeScript types and interfaces in .types.ts:
// 🏷️ Types
export interface ParsedEntry {
word: string;
partOfSpeech: string;
}
Do not re-export types from index.ts unless they are part of the public API consumed by other modules.
Registering in the root module
After generating a module, import it in main.module.ts:
@Module({
imports: [
ConfigModule.forRoot({ ... }),
LoggerModule,
MyDomainModule, // ← add here
],
providers: [],
})
export class MainModule {}
Conformetry validation
Conformetry validation measures generated and existing module structures against the templates they came from. It runs for one project, or for every project at once.
pnpm nx run-many --targets=conformetry-validate
Best Practices
- Never put business logic in
main.ts— it bootstrapsCommandFactoryonly. - One command per class — split sub-commands into separate
CommandRunnersubclasses. - Validate at the boundary — all env vars must be declared in
environmentSchema; access viaConfigService, notprocess.env. - Type imports — use
import { type Foo }for type-only imports (enforced by ESLint). - No
anytypes — useunknownor proper typing; strict mode is enabled.
See the write-typescript skill for strict mode patterns.
Troubleshooting
- Command not found at runtime — ensure the command class is listed in
providersof its module and the module is imported by the root module. - Dependency injection failure — verify the service is
@Injectable(), exported from its module, and that module is imported by the consuming module. - Unrecognized CLI flag — check that
@Option()decorators in the command class exactly match the flag names passed. - Env var validation error on startup — add the missing variable to
environmentSchemainsrc/constants.tsand to.env.default.
See the triage-integration skill for lint and git hook failures.
Key Files
- src/main.ts: Application bootstrap
- src/main.module.ts: Root NestJS module
- src/constants.ts:
environmentSchema(Zod) @codebase/logger(packages/logger): shared pino-backedLoggerServiceandLoggerModule- project.json: Nx targets (
develop,build,test,lint,typecheck,format) - .env.default: Environment variable template