Nestjs crud pattern
A lean agent harness for AI-assisted development — NestJS + TypeScript + Prisma + PostgreSQL. Skills, rules and commands extracted from real projects.
npx -y skills add JoaoEquer/Oficina --skill nestjs-crud-patternAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 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.
What its author says it does
Copied from the file, not written here
House pattern for building complete CRUD domains in NestJS with Prisma — controller/service/repository with dependency inversion, validated DTOs and multi-tenant isolation. Use whenever creating a new module, entity, CRUD route or domain in a NestJS backend, even if the request is just "create the X route" or "add the Y table".
SKILL.md
4.1 KB, as published. Nobody here has run it
NestJS CRUD — house pattern
Every CRUD domain follows exactly the same shape. The project's first module is the mold; the rest copy its form. Do not invent variations.
Per-domain structure
src/modules/<domain>/
├── <domain>.controller.ts # HTTP only: receives request, returns response. Zero business logic.
├── <domain>.service.ts # Business logic. Depends on the repository ABSTRACTION, never on Prisma directly.
├── <domain>.repository.ts # Abstract class (contract) + Prisma implementation in the same file.
├── dto/
│ ├── create-<domain>.dto.ts
│ └── update-<domain>.dto.ts
└── <domain>.module.ts # Wiring: binds the contract to the implementation via provider token.
Cross-cutting concerns (PrismaService, guards, context decorators) live in src/shared/.
The repository contract (DIP in practice)
// <domain>.repository.ts
export abstract class TaskRepository {
abstract create(workspaceId: string, data: CreateTaskDto): Promise<Task>;
abstract findAll(workspaceId: string): Promise<Task[]>;
abstract findById(workspaceId: string, id: string): Promise<Task | null>;
abstract update(workspaceId: string, id: string, data: UpdateTaskDto): Promise<Task>;
abstract softDelete(workspaceId: string, id: string): Promise<void>;
}
@Injectable()
export class PrismaTaskRepository extends TaskRepository {
constructor(private readonly prisma: PrismaService) { super(); }
// ... implementation: EVERY query filters by workspaceId
}
// <domain>.module.ts
@Module({
controllers: [TaskController],
providers: [
TaskService,
{ provide: TaskRepository, useClass: PrismaTaskRepository },
],
})
export class TaskModule {}
The service injects TaskRepository (the contract). Swapping the implementation or mocking in tests becomes trivial.
Non-negotiable rules
workspaceIdalways comes from the authenticated context (JWT), never from the request body. If Auth doesn't exist yet, use a marker constant (FAKE_WORKSPACE_ID) with a// TODO: extract from JWTcomment — and register it as a visible pending item.- Tenant isolation enforced at the data layer: every repository query filters by
workspaceId. No exceptions, not even for "internal" queries. - Permissions validated on the server (guard/decorator), never trusted to the frontend.
- Dates always in UTC.
- DTOs with
class-validatorand a globalValidationPipeenabled inmain.ts(whitelist: true). - Delete is soft delete by default (
deletedAt), unless an explicit decision says otherwise.
SOLID, feet on the ground
- SRP: one responsibility per layer. The controller doesn't validate business rules; the service doesn't build SQL; the repository doesn't decide policy.
- DIP: service ↔ repository contract, as above.
- OCP/ISP without excess: do NOT create a generic
BaseRepository<T>, do NOT build hexagonal ports/adapters, do NOT abstract what isn't needed yet. Abstraction only where the cost pays off.
Source of truth for the model
The exact fields of each entity come from the project's data model document (UML/diagram in docs/). If the document doesn't exist or the entity isn't in it, stop and ask before inventing fields. Never run a production migration without checking the schema against the source of truth.
Definition of done (per domain)
- Model in Prisma + migration applied (table actually created)
- Complete module: POST, GET list, GET by id, PATCH, DELETE responding
-
workspaceIdfrom context, filtered queries, DTOs validating - Lint and build clean, project boots locally
- Registered in
AppModule - Documented: the work goes into the day's delivery log (see
git-workflowrule)