Imported from mariopedemonte12/mario-da-parfums-v2 (
backend/.claude/skills/module-standards/SKILL.md). Install upstream withnpx skills add mariopedemonte12/mario-da-parfums-v2 --skill module-standards. Copyright stays with the author.
Backend module standards
Minimum quality bar for any backend module you create or modify. These apply on top of the conventions in backend/CLAUDE.md, not instead of them — check that file first for stack/layering/DTO conventions, then apply this on top.
- Everything travels through a validated DTO — no exceptions: never accept a raw
@Body()/@Query()/@Param()as a loosely-typed object.class-validatorenforces the shape; when the raw input needs converting (query-stringpage/limit→number, a comma-separated filter → array, trimming/normalizing a string) useclass-transformer(@Type,@Transform) or a pipe insrc/pipes, not manual parsing in the controller/service. Prefer the wrappers insrc/validators/wrappers(IsRequired,IsStringField,IsEmailField,IsEnumField,MinLen/MaxLen, ...) over bareclass-validatordecorators — they attach aValidationErrorCodefor you. If no wrapper or stock decorator fits, write a custom validator undersrc/validators(seeis-not-profane.validator.ts,is-password-strong.validator.ts) instead of inlining regex/logic in the DTO. - Never let a field settle for a looser type than its real business contract: pick the tightest validator the field's actual constraint allows.
- URLs —
@IsUrl(), withprotocols/require_tldnarrowed to what's actually accepted, not a bare string. - Integers / numeric ranges —
@IsInt()plus@Min()/@Max()for anything with a real floor or ceiling (price, stock, rating,page/limit); for query params add@Type(() => Number), since query values always arrive as strings. - S3 object keys —
@Matches(<pattern>)against the bucket's actual key convention, never just@IsString(). (photoS3Keyincreate-user.dto.tstoday only checks type — that's a pre-existing gap to close when you touch that DTO, not the bar to copy. A key that doesn't match the expected prefix/shape must fail validation before it ever reaches an S3 call.) Factor it into asrc/validators/wrappershelper once a second DTO needs the same pattern. - Enums / fixed vocabularies —
IsEnumField, never a bare string. - Anything else with a business-defined shape (SKU format, slug, currency code, date range) gets a decorator or
@Matchespattern matching that shape — the DTO is where the business contract is enforced, not a downstream service check. - Every rule needs a reason to fail with: pass a
ValidationErrorCode(src/shared/enums/validation-error-code.enums.ts) when the field must be distinguishable from other fields of the same type, perdocs/validation-error-codes.md— never a string literal. A rule that can fail for several reasons at once on one value (like password strength) gets its own dedicated error-code enum instead of overloadingValidationErrorCode.
- URLs —
- Know the error system before implementing (full detail in
docs/error-handling.md) —customValidationPipe(src/pipes/custom-validation.pipe.ts) andAllExceptionsFilter(src/common/filters/http-exception.filter.ts) are already wired globally inmain.ts; don't build parallel error handling in a controller or service.- A DTO validation failure becomes
400 { statusCode, message: 'Validation failed', errors: FieldError[], timestamp }, where eachFieldErroris{ field, errors: { code, meta? }[] }— this is what lets a failing input be traced back to why it failed, field by field, code by code, instead of just "bad request." - A business-rule failure (duplicate email, not found, forbidden, ...) throws Nest's built-in HTTP exceptions directly (
ConflictException,NotFoundException,ForbiddenException,UnauthorizedException, ...) — no custom exception hierarchy exists or is needed at this scale. The filter normalizes it to the same envelope, withouterrors(no per-field detail to carry). - An unexpected error (DB down, uncaught bug) is logged server-side and returned to the client as a generic
500 Internal server error— the original message/stack is never leaked. - Never
throw new Error(...)for an expected business case — it falls into the generic-500 branch, turning a case the client could handle (e.g. "email already registered") into an opaque failure.
- A DTO validation failure becomes
- Batch endpoints on every CRUD module: alongside the single-item endpoints, expose batch equivalents so bulk loads/edits don't cost one request per row:
POST /<resource>/batch— create many, body{ items: CreateXDto[] }(@ValidateNested({ each: true }) @Type(() => CreateXDto)onitems).PATCH /<resource>/batch— update many, body{ items: (UpdateXDto & { id: number })[] }.DELETE /<resource>/batch— delete many, body{ ids: number[] }.- Batch responses report a per-item outcome (e.g.
{ id, success, error? }) so a partial failure doesn't get silently swallowed — batches are partial-success by default unless the module's spec explicitly calls for all-or-nothing (transactional) semantics. - Do not keep the single-item endpoints.
- Server-side filtering and pagination on every list endpoint:
GET /<resource>takes a validatedFindXDto/XFilterDtoquery DTO; filters are applied as Drizzlewhereclauses, never by fetching everything and filtering in application code. Pagination (page/limit, with an enforced default and maxlimit) is always applied server-side, never left to the client to slice a full result set. Once a second module needs the same paging shape, factor it into a shared DTO/decorator insrc/commonand reuse it instead of redefining paging per module. - Seeds: a module whose entities other modules depend on (FK-referenced) should ship seed data so the test database can be populated deterministically. Seed scripts must run in dependency order — a module seeds after every module it references (e.g.
vendorsbeforelistings) — expressed explicitly in the seed runner, not left to file/alphabetical ordering. Skip seeds for a module when nothing depends on its data; don't add them reflexively. - Every endpoint and DTO field is documented with Swagger (
@nestjs/swagger): the API is self-documenting via OpenAPI — undocumented surface is incomplete work, not a follow-up.- Bootstrap (
src/main.ts):SwaggerModule.setup(...)with aDocumentBuilderis wired globally, same ascustomValidationPipe/AllExceptionsFilter— if it isn't there yet when you touchmain.ts, add it rather than working around its absence. - Controllers:
@ApiTags('<resource>')on the class; every handler gets@ApiOperation({ summary: ... })plus@ApiResponsefor the success status and for each documented failure mode (400validation,401/403auth,404not found,409conflict, ...) — reuse the shape fromdocs/error-handling.md's envelope, don't invent a different documented error shape than what the filter actually returns. - DTOs: every field gets
@ApiProperty()(or@ApiPropertyOptional()for optional fields) describing what theclass-validatordecorators on the same field already enforce — type,example,enum,minimum/maximum,pattern, whichever apply. The Swagger doc must match the real validation contract, not a looser paraphrase of it: if a field is@IsInt() @Min(0) @Max(100), its@ApiPropertydeclares that range, not justtype: Number. - Response DTOs (what a controller actually returns, e.g.
ResponseUserDto) get the same@ApiProperty()treatment so the documented response shape matches what's serialized, not just the request side. - Batch endpoints document the per-item outcome shape (
{ id, success, error? }) as its own response DTO — don't leave a batch response as an undocumentedany. - Keep Swagger annotations in the DTO/controller files themselves (next to the
class-validatordecorators they describe), not in a separate doc file — they're generated from the code, so drift is only avoidable if they live where the code changes.
- Bootstrap (
- Module isolation: a module has one explicitly stated responsibility — say what it owns (and doesn't own) in a one-line comment on the module class or in its
NOTES.md(root convention) if one exists. If you can't state a module's job in one sentence, it's probably two modules; split it rather than growing a catch-all module. - Changing this skill: this is a shared quality bar — edit it only with the user's explicit agreement, not unilaterally while implementing a feature.