Implicit
jg-nestjs
Modules, schema validation, configuration, logging, lifecycle, and tests for NestJS.
- Usage
- Loads automatically
When to use it
It loads on its own when a task touches NestJS modules, controllers, providers, or tests. Call it by name to load it before such a task.
When it loads
Your agent loads the skill on its own when a request like this touches its area.
Add an endpoint that archives a project by its ID.
Validates the ID with a schema, keeps the controller thin, logs through the Nest logger, and tests the module through dependency injection.
SKILL.md
What the agent reads
The complete instructions your agent loads when a task touches its area.
Application
The agent loads this skill on its own when a task touches its area. This skill’s area is NestJS modules, controllers, providers, guards, pipes, interceptors, configuration, logging, and NestJS tests.
- It adds domain rules to the task. It never authorizes actions, widens scope, or replaces the workflow or approvals of a skill the developer invoked.
- It ranks with the repository’s coding instructions. When they conflict, the coding instructions win, except for the exceptions this skill states for its own area.
- The repository’s established conventions win over this skill’s naming, layout, and style rules. Its correctness and safety rules, and the exceptions it states for its own area, still apply.
- Check the installed versions first. This skill was written for NestJS 12. An item marked with a minimum version, written like (18+), applies only when that version or a later one is installed. For an older installed version, follow that version’s official documentation.
- Before relying on one of this skill’s version-specific items, check it against the current official guidance when the installed version of the technology is newer than the one this skill was written for, or when the version bullet gives a review month for something without an installed version, such as a hosted service or browser behavior, and that month is more than six months before the current date. Where an item differs from that guidance, follow the guidance. Version-specific items are API, syntax, and feature recommendations, and anything marked with a minimum version. This skill’s conventions and requirements, such as its naming, layout, style, reuse, and safety rules, keep their usual precedence even where official guidance suggests otherwise. Report each item that looks outdated in the summary, with the installed version and what the current guidance says, and offer there to open an issue on the jg-skills repository,
https://github.com/jesusgraterol/jg-skills:- Never pause or stop a run for the offer, including an autonomous run. When no one can answer, include the pre-filled link described below instead of asking.
- First search the repository’s open issues, and skip the offer when one already covers the item. When the search can’t run, say so next to the link, so the developer checks the existing issues before submitting.
- Open an issue only after the developer agrees, because it is public. Title it
<skill>: <item> looks outdated on <technology> <version>, and include only the skill name, the outdated item, the installed version, and a link to the official documentation, never code, file paths, or project details. Add no label. - Use
gh issue createwhenghis installed and signed in. Otherwise give the developer a pre-filled link,https://github.com/jesusgraterol/jg-skills/issues/new?title=…&body=…, with both values URL-encoded.
- This skill isn’t a complete list. Where it says nothing, follow the official guidance for the installed version, and in new code the task touches, prefer the features that guidance currently recommends over older ones that do the same job, unless the repository has an established pattern for that job. Before using a newer approach instead of one this skill gives, confirm it in that version’s official documentation, never from memory alone, and report the item as outdated as described above. When that documentation can’t be reached, keep this skill’s approach and report that the newer approach couldn’t be confirmed, without pausing the run.
- Rules that say “prefer” are defaults: departing from one with a stated reason is fine. Rules that say “never”, “always”, or “must” are requirements.
- Nothing in this skill requires adding a dependency, enabling a compiler or tool, upgrading, or migrating existing code.
NestJS module organization
- Place each NestJS module in its own directory.
- Use framework artifact suffixes such as
.module.ts,.service.ts,.controller.ts, and.guard.ts; use the module-prefixed role files below for supporting singletons. - Do not place NestJS module, service, controller, guard, interceptor, pipe, filter, resolver, or provider classes directly in a parent directory when they represent a meaningful module unit.
- A domain module that owns orchestration or business logic must have exactly one primary service:
<module>.service.tscontaining<Module>Service. - Do not add role-qualified services within the same module, e.g.
<module>-cache.service.ts,<module>-model.service.ts, or<module>-validations.service.ts. - Name supporting injectable/stateful singletons by role without
Service:<module>.model.ts/<Module>Model,<module>.cache.ts/<Module>Cache, and<module>.validations.ts/<Module>Validations. - Allow a separately named service only for an independent domain or integration responsibility, in its own child module with the normal primary-service convention.
- Role terms in domain names do not alter this convention:
ApiAdminCacheServiceis correct for anApiAdminCachemodule; anAdminmodule’s cache singleton isAdminCacheinadmin.cache.ts, notAdminCacheService. <module>.validations.tsmay own the injectable<Module>Validationssingleton; unprefixedvalidations.ts,utilities.ts, andtransformers.tsmust contain only pure functions.- Name tests for supporting singletons after the artifact, such as
<module>.cache.test-unit.ts,<module>.model.test-integration.ts, and<module>.validations.test-unit.ts. - Keep
index.tsas a thin public import boundary. Export supporting singletons only when they are genuinely consumed outside the module. - A module directory’s entry file exports only externally consumed NestJS modules, providers, services, controllers, guards, contracts, and other public symbols, each re-exported from its owning file: primary services from
<module>.service.ts, and supporting singletons from<module>.model.ts,<module>.cache.ts, or<module>.validations.ts. - Exception to the repository’s coding instructions: do not add comments or JSDoc above NestJS artifact class declarations, such as modules, services, controllers, and guards. Document non-obvious contracts on their members or in module documentation instead.
A module, with only the artifacts it needs. The supporting singleton and pure-function files are optional:
example/
index.ts
example.module.ts
example.service.ts
example.controller.ts
example.guard.ts
example.model.ts
example.cache.ts
example.validations.ts
example.exception.ts
types.ts
constants.ts
utilities.ts
transformers.ts
validations.ts
example.service.test-unit.ts
example.cache.test-unit.ts
example.model.test-integration.ts
example.validations.test-unit.ts
example.exception.test-unit.tsControllers and validation
Keep controllers thin and validate their input at the edge with the schema that defines it:
@Controller('goals')
class GoalsController {
public constructor(private readonly goalsService: GoalsService) {}
@Delete(':id')
public deleteGoal(
@Param('id', { schema: z.coerce.number().int().positive() }) id: number,
): Promise<void> {
return this.goalsService.deleteGoal(id);
}
}- (12+) Validate
@Body(),@Query(), and@Param()input by passing a Zod schema as{ schema }, withStandardSchemaValidationPiperegistered, for example throughapp.useGlobalPipes. With@nestjs/swaggerand Zod 4.2 or later, or a configuredstandardSchemaConverter, the same schema also feeds the OpenAPI document, so validation and documentation cannot drift apart. - (12+) Give
ConfigModulea ZodvalidationSchema, so invalid configuration stops the application at startup instead of failing on first use. - Verify webhook signatures over the raw request bytes, never over a re-serialized parsed body: create the application with
rawBody: trueand readreq.rawBodythroughRawBodyRequest, or use araw()body parser on the webhook route only, and give the parser an explicit bodylimit. - Send
Cache-Control: no-storeon responses that carry credentials, tokens, capability URLs, or personal data, and prefer it for every API that browsers call. Decide it by what the response contains, not by whether a guard protects the route. - Set Express’s
trust proxyto the exact number of proxy hops or the proxies’ addresses, nevertrue, which trusts the left-mostX-Forwarded-Forentry that a client can forge. - A custom throttler
getTrackermust group IPv6 addresses by subnet itself, such as by /64. (@nestjs/throttler6.7+) Only the built-in tracker groups them, and the package exportsnormalizeIp()for custom trackers. - With several replicas, propose shared throttler storage, such as Redis, instead of the default in-memory storage, which counts each process separately.
NestJS logging
In NestJS projects:
- Import
Loggerfrom@nestjs/common. - Do not use direct
console.log,console.error,console.warn, or similar calls. - Use the appropriate
Loggermethod:log,error,warn,debug,verbose, orfatal. - In services, define a private readonly logger with the service class name as context.
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
class MyService {
private readonly __logger = new Logger(MyService.name);
public doSomething(): void {
this.__logger.log('Doing something...');
}
}(12+) Pass log context as structured parameters after the message, such as this.__logger.log('Goal deleted', { goalId }), instead of interpolating it into the message.
Lifecycle and providers
- Call
app.enableShutdownHooks()in applications that hold connections, and close pools, queues, and consumers inonModuleDestroyorbeforeApplicationShutdown. (12+) Within a module, a provider’s lifecycle hooks run after those of the providers it depends on, and teardown runs in reverse. Do not rely on the order between providers at the same level or between sibling modules. - Prefer default singleton providers. A request-scoped provider makes every provider that injects it request-scoped too, so the whole chain is rebuilt on each request. Use
AsyncLocalStoragewhen code needs request context. - Keep the repository’s module format, CommonJS or ESM, when adding files or packages.
Scheduled jobs
@Interval()runs onsetInterval: its first run comes one interval after startup, runs can overlap, and@nestjs/scheduleclears it only inbeforeApplicationShutdown. Keep the guards inside the method: skip a run while the previous one is active, and start no run once shutdown begins. Await the active run inonModuleDestroy, and close the pools and clients a job uses inbeforeApplicationShutdown, which runs after every module’sonModuleDestroy, so a run never loses its connection partway. For@Cron(), (@nestjs/schedule5.0.1+)waitForCompletion: trueskips ticks while a run is active.- Bound each run to a batch of the oldest due rows, and isolate failures between its stages, so one failing stage doesn’t stop the others.
@nestjs/scheduleruns its jobs in every replica. Run each job in one designated process, or behind a lock shared by all replicas. For the lock, propose the official@nestjs/locks(0.0.3, pre-1.0) with a shared store, rather than adding it; its default in-memory store doesn’t coordinate replicas. Keep each run safe to overlap anyway, because a lock can expire mid-run, for example by claiming rows as jg-postgres describes.
NestJS test module lifecycle
Use Test.createTestingModule(...) from @nestjs/testing when tests require NestJS modules or dependency injection.
Requirements:
- create the module with
await Test.createTestingModule({...}).compile() - store the compiled module in a
TestingModulevariable - call
await module.init()after compile - call
await module.close()during teardown - prefer
beforeAll/afterAllwhen the module is shared across the suite - do not bypass dependency injection with ad-hoc manual instantiation when the class relies on NestJS providers, dependency injection, or lifecycle hooks
- do not omit
module.init()ormodule.close() - when a dependency should be mocked, override its provider with
useValue - in integration tests, keep behavior-relevant owned dependencies real and apply the repository’s test-double rules; do not replace the interaction or infrastructure behavior the test must verify
- retrieve the class under test with
module.get(...) - never mock the class under test itself
- keep necessary mocks minimal but compatible with used methods/properties
For NestJS HttpService dependencies, compile with real-setup-compatible HTTP imports/providers: include HttpModule in imports unless intentionally overridden or mocked.
import { Test, type TestingModule } from '@nestjs/testing';
import { afterAll, beforeAll, describe, test } from 'vitest';
describe('MyService', () => {
let module: TestingModule;
let service: MyService;
beforeAll(async () => {
module = await Test.createTestingModule({
providers: [MyService],
}).compile();
await module.init();
service = module.get(MyService);
});
afterAll(async () => {
await module.close();
});
test('performs the expected behavior', async () => {
// test meaningful application behavior
});
});