Nestjs architecture principles
Skill benedya/nestjs-skills/skills/nestjs-architecture-principles
Architecture and coding conventions for this NestJS + TypeORM backend: request-flow layering (controller → service → repository → entity), module boundaries, SOLID / DRY / single-responsibility, service and business-logic patterns, dependency injection, TypeORM entities and custom repositories, DTOs, file/import organization, and testing strategy. Use when designing, adding, or editing any module, controller, service, repository, or entity, or when deciding where code should go, how to structure a module, name files, inject dependencies, define entities/columns/indexes, write queries, or expose data safely. Trigger on phrases like "add a service/module/entity/controller", "business logic", "inject a dependency", "custom repository", "extend Repository", "query builder", "which layer", "where should this go", "new table", "add a column", or any work under controllers/, services/, repositories/, entities/, or dtos/.From its SKILL.md
npx -y skills add benedya/nestjs-skills --skill nestjs-architecture-principlesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
9.0 KB, ~1.8k tokens by cl100k_base, as published. Nobody here has run it
NestJS Architecture Principles
How a feature is layered, the principles every change follows, and the per-layer rules for services and TypeORM persistence.
Request flow
HTTP → Controller → Service → Repository → Entity / DB
Keep each layer to its one job:
- Controller (thin) — HTTP only: routing, guards, validation, Swagger. No business logic; delegates to one service.
- Service — business logic for one use-case; orchestrates repositories and other services. See Services.
- Custom repository — owns all DB access for an entity via intent-named methods. See Persistence (TypeORM).
- Entity — the persistence shape; never leaves its module.
Core principles (non-negotiable)
- SOLID. Follow single-responsibility, open/closed, Liskov, interface segregation, and dependency inversion.
- DRY. Don't repeat logic — extract shared behaviour into a helper, repository method, or service instead of copy-pasting.
- One service = one use-case. Small, high-cohesion, named for the action
(
CreateArticleService), composed together — never a god-service. - No pass-through methods — never add a method that merely delegates without adding logic, transformation, or error handling.
- Controllers stay thin — no business logic in the HTTP layer.
- Repositories own queries. Inject the custom repository class, not generic
Repository<Entity>; add intent-named methods (findPublishedByAuthorId). - Deep modules, high cohesion, low coupling. Hide substantial behavior behind a small surface (few exports, simple types); keep what's inside tightly related. Avoid shallow modules — thin wrappers or splits that only look modular.
- Module boundaries. A module exports services only — never repositories or entities; cross-module communication uses DTOs/types, never entities.
- Validate at the edge.
class-validatorfor HTTP DTOs; a startup schema (e.g. Zod) for config. - Expose data safely. Return response DTOs, not entities; expose only the
fields the consumer needs; never leak secrets (passwords, API keys, tokens).
If an entity must be returned, make exposure opt-in with serialization groups
(
@Exclude+@Expose({ groups })).
Services
Business logic lives here — one class per use-case.
- Single Responsibility & high cohesion. A service owns one clearly defined
responsibility, and every method contributes to that same responsibility. Name
the file and class for the action, not the entity:
create-article.service.ts→CreateArticleService,get-article-list.service.ts→GetArticleListService. - Prefer composition — build a workflow by injecting and orchestrating focused services rather than growing one large service.
- Avoid pass-through methods: do not add a method that merely delegates to another service or repository without adding logic, transformation, or error handling. Pass-throughs add indirection without value.
- Inject custom repository classes directly (e.g.
ResourceRepository), not the genericRepository<Entity>— see Persistence (TypeORM). - Mark constructor-injected dependencies
private readonly— it makes the DI intent explicit and prevents reassignment inside the class. - Initialize
Loggerwith the class name (new Logger(ClassName.name)) so log lines are traceable to their source.
import { Injectable, Logger } from '@nestjs/common'
@Injectable()
export class ResourceService {
private readonly logger = new Logger(ResourceService.name)
constructor(
private readonly resourceRepository: ResourceRepository,
private readonly otherService: OtherService
) {}
}
Persistence (TypeORM)
Conventions for entities and custom repositories.
Entities
- Always specify
name(snake_case) in@Entity()and@Column()so renaming a TS property never silently breaks the DB mapping. - Mark nullable columns as
TypeA | nullso the TS type matches the DB and callers can't forget to handlenull. - Use predictable names for constraints and indexes so DB introspection stays
consistent:
pk_<table>_idfor primary keys,idx_<table>_<column(s)>for indexes (single or composite), anduq_<table>_<column(s)>for unique indexes.
import { Column, Entity, Index, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
@Entity({ name: 'table_name' })
@Index('idx_table_name_column', ['columnName'])
export class SomeEntity {
@PrimaryGeneratedColumn('identity', {
primaryKeyConstraintName: 'pk_table_name_id',
})
id: number
@Column({ name: 'column_name', type: 'varchar', nullable: true })
columnName: string | null
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'NOW()' })
updatedAt: Date
}
Custom repositories
Create a custom repository class per entity instead of injecting the generic
Repository<Entity> — it keeps queries type-safe and gives the module a clean
injection target.
- Inject the custom repository class into services (not
Repository<Entity>). - Do not export repositories from a module's
exports; expose services as the module's entry points (encapsulation). - Prefer adding dedicated methods on the custom repository (e.g.
findPublishedByAuthorId(id),existsBySlug(slug)) instead of calling generic TypeORM methods likefind,findOne,findBy,count, orcreateQueryBuilderdirectly from services. Services express intent ("get published articles by author"); the repository owns the query shape. - Prefer entity property names over raw SQL column names in query builders,
findoptions, andwhereclauses (columnName, not'column_name') — this stays type-safe and survives column renames. - Extend
Repository<YourEntity>and decorate with@Injectable()so NestJS DI can resolve it. - Inject
DataSourceand callsuper(EntityClass, dataSource.createEntityManager()). - Place repositories in
repositories/.
import { Injectable } from '@nestjs/common'
import { DataSource, Repository } from 'typeorm'
import { ArticleEntity } from '../entities/article.entity'
@Injectable()
export class ArticleRepository extends Repository<ArticleEntity> {
constructor(dataSource: DataSource) {
super(ArticleEntity, dataSource.createEntityManager())
}
}
Register it as a provider:
@Module({
providers: [ArticleRepository],
})
export class ArticlesModule {}
Code organization
- One type per file. Group types in one file only when highly cohesive and it improves readability.
- Never suffix type/interface files with
.type/.interface/.types— name them for the concept:article-filters.ts, notarticle-filters.interface.ts(thetypes/folder already says what's inside). - No barrel files (e.g.
index.tsre-exporting a folder). Import directly from the defining file. - Use the project's TypeScript path aliases; never cross-module relative imports
(same-module relative like
../entities/foo.entityis fine). New module → register its alias intsconfig.json. The alias list is project-specific (seetsconfig.json).
Testing strategy
- Repositories & controllers/endpoints → integration tests.
- Services & helpers/utilities → unit tests.
Module structure (reference)
Each feature module typically contains — REST handlers in controllers/ — so
every module reads the same way:
module-name/
controllers/ # HTTP endpoints; thin, delegate to a service
dtos/ # request/response payloads, class-validator decorated
entities/ # TypeORM mappings to DB tables
repositories/ # custom repository per entity, owns queries
services/ # business logic, one class per use-case
types/ # shared types
utils/ # pure helper functions
pipes/ # request validation and transformation
errors/ # domain-specific error classes
constants/ # fixed values
What ships with it: 1 file
3.8 KB alongside SKILL.md
- README.md3.8 KB