CQRS Command Query Generator
About
This Claude Skill generates CQRS pattern components like Commands, Queries, and their Handlers for separating write and read operations. Use it when creating use cases, read models, or when terms like "CQRS," "command," or "query" are mentioned. It helps structure your application layer by producing the corresponding code artifacts.
Quick Install
Claude Code
Recommended/plugin add https://github.com/majiayu000/claude-skill-registrygit clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/CQRS Command Query GeneratorCopy and paste this command in Claude Code to install this skill
Documentation
CQRS Command Query Generator
đŻ Mission
Créer des Commands et Queries suivant le pattern CQRS pour séparer les opérations d'écriture (commands) des opérations de lecture (queries) dans l'Application Layer.
đ Philosophie CQRS (Command Query Responsibility Segregation)
Principe Fondamental
CQRS sépare les responsabilités en deux types d'opérations :
-
Commands (Ăcritures) : Modifient l'Ă©tat du systĂšme
- Create, Update, Delete
- Retournent un ID ou un booléen de succÚs
- Peuvent échouer (validation, business rules)
-
Queries (Lectures) : Lisent l'état du systÚme
- Get, List, Search
- Retournent des Read Models (DTOs optimisés)
- Ne modifient JAMAIS l'état
Avantages
- â SĂ©paration claire : Ăcritures vs Lectures
- â Optimisation indĂ©pendante : Read Models optimisĂ©s pour l'UI
- â ScalabilitĂ© : PossibilitĂ© de scaler reads et writes sĂ©parĂ©ment
- â Payloads minimaux : Commands retournent juste un ID
- â MaintenabilitĂ© : Ajout de nouvelles opĂ©rations sans affecter l'existant
Architecture CQRS dans le Projet
application/
âââ commands/ # Write operations
â âââ create-club/
â âââ create-club.command.ts # Command DTO
â âââ create-club.handler.ts # Command Handler
â âââ create-club.spec.ts # Tests
âââ queries/ # Read operations
â âââ get-club/
â âââ get-club.query.ts # Query DTO
â âââ get-club.handler.ts # Query Handler
â âââ get-club.spec.ts # Tests
âââ read-models/ # Optimized DTOs for UI
âââ club-detail.read-model.ts
âââ club-list.read-model.ts
âââ index.ts
đïž Commands (Write Operations)
Qu'est-ce qu'un Command ?
Un Command représente l'intention de l'utilisateur de modifier l'état du systÚme.
Caractéristiques :
- Nommé avec un verbe d'action :
CreateClub,UpdateSubscription,DeleteMember - Contient toutes les données nécessaires à l'opération
- Validé avec class-validator
- Immuable (DTO, pas de setters)
- Co-localisĂ© avec son handler dans le mĂȘme dossier
Template Command
// application/commands/create-club/create-club.command.ts
import { IsString, IsNotEmpty, IsOptional, MaxLength } from 'class-validator';
export class CreateClubCommand {
@IsString()
@IsNotEmpty()
@MaxLength(100)
readonly name: string;
@IsString()
@IsOptional()
@MaxLength(500)
readonly description?: string;
@IsString()
@IsNotEmpty()
readonly userId: string; // ID de l'utilisateur qui crée le club
constructor(name: string, description: string | undefined, userId: string) {
this.name = name;
this.description = description;
this.userId = userId;
}
}
Template Command Handler
// application/commands/create-club/create-club.handler.ts
import { Injectable, Inject } from '@nestjs/common';
import { CreateClubCommand } from './create-club.command';
import { IClubRepository, CLUB_REPOSITORY } from '../../domain/repositories/club.repository.interface';
import { Club } from '../../domain/entities/club.entity';
import { ClubName } from '../../domain/value-objects/club-name.vo';
@Injectable()
export class CreateClubHandler {
constructor(
@Inject(CLUB_REPOSITORY)
private readonly clubRepository: IClubRepository,
) {}
async execute(command: CreateClubCommand): Promise<string> {
// 1. Créer l'entité domain avec la logique métier
const clubName = ClubName.create(command.name);
const club = Club.create(
clubName,
command.description,
command.userId,
);
// 2. Persister via le repository
const savedClub = await this.clubRepository.create(club);
// 3. Retourner UNIQUEMENT l'ID (payload minimal)
return savedClub.getId();
}
}
RĂšgles pour les Commands
- â Un Command = Une responsabilitĂ© unique
- â Validation avec class-validator
- â Retourne un ID (string) ou void
- â Orchestre les entitĂ©s domain (pas de logique mĂ©tier)
- â GĂšre les erreurs mĂ©tier (throw domain exceptions)
- â JAMAIS de logique mĂ©tier (celle-ci est dans le Domain)
- â JAMAIS de retour de Read Model (utiliser une Query aprĂšs)
- â JAMAIS d'accĂšs direct Ă Prisma (utiliser le repository)
Exemples de Commands
// Write operations (modifient l'état)
CreateClubCommand
UpdateClubCommand
DeleteClubCommand
SubscribeToPlanCommand
UpgradeSubscriptionCommand
GenerateInvitationCommand
AcceptInvitationCommand
RemoveMemberCommand
ChangeClubCommand
đ Queries (Read Operations)
Qu'est-ce qu'une Query ?
Une Query représente l'intention de l'utilisateur de lire des données sans modifier l'état.
Caractéristiques :
- Nommée avec l'intention de lecture :
GetClub,ListClubs,SearchMembers - Contient les paramĂštres de filtrage (pagination, sorting, filtering)
- Validée avec class-validator
- Immuable (DTO)
- Co-localisée avec son handler
Template Query
// application/queries/list-clubs/list-clubs.query.ts
import { IsOptional, IsNumber, Min, Max, IsString } from 'class-validator';
export class ListClubsQuery {
@IsOptional()
@IsNumber()
@Min(1)
readonly page?: number = 1;
@IsOptional()
@IsNumber()
@Min(1)
@Max(100)
readonly limit?: number = 10;
@IsOptional()
@IsString()
readonly search?: string;
@IsOptional()
@IsString()
readonly userId?: string; // Filtrer par utilisateur
constructor(page?: number, limit?: number, search?: string, userId?: string) {
this.page = page ?? 1;
this.limit = limit ?? 10;
this.search = search;
this.userId = userId;
}
}
Template Query Handler
// application/queries/list-clubs/list-clubs.handler.ts
import { Injectable, Inject } from '@nestjs/common';
import { ListClubsQuery } from './list-clubs.query';
import { IClubRepository, CLUB_REPOSITORY } from '../../domain/repositories/club.repository.interface';
import { ClubListReadModel } from '../../read-models/club-list.read-model';
import { PaginatedResult } from '../../../shared/types/paginated-result';
@Injectable()
export class ListClubsHandler {
constructor(
@Inject(CLUB_REPOSITORY)
private readonly clubRepository: IClubRepository,
) {}
async execute(query: ListClubsQuery): Promise<PaginatedResult<ClubListReadModel>> {
// 1. Récupérer les entités domain via le repository
const result = await this.clubRepository.findAll({
page: query.page,
limit: query.limit,
search: query.search,
userId: query.userId,
});
// 2. Transformer les entités en Read Models (optimisés pour l'UI)
const data = result.data.map(club => this.toReadModel(club));
// 3. Retourner les Read Models avec métadonnées de pagination
return {
data,
meta: {
page: query.page,
limit: query.limit,
total: result.total,
totalPages: Math.ceil(result.total / query.limit),
},
};
}
private toReadModel(club: Club): ClubListReadModel {
return {
id: club.getId(),
name: club.getName().getValue(),
description: club.getDescription(),
membersCount: club.getMembersCount(),
createdAt: club.getCreatedAt(),
};
}
}
RĂšgles pour les Queries
- â Une Query = Une intention de lecture
- â Retourne des Read Models (JAMAIS les entitĂ©s domain)
- â Transforme Domain Entities â Read Models
- â Optimise pour l'UI (sĂ©lection des champs pertinents)
- â Supporte pagination, filtrage, sorting
- â JAMAIS de modification d'Ă©tat
- â JAMAIS de retour des entitĂ©s domain brutes
- â JAMAIS de logique mĂ©tier
Exemples de Queries
// Read operations (ne modifient PAS l'état)
GetClubQuery
ListClubsQuery
GetSubscriptionQuery
ValidateInvitationQuery
ListMembersQuery
SearchClubsQuery
đ Read Models (Optimized DTOs)
Qu'est-ce qu'un Read Model ?
Un Read Model est un DTO optimisé pour une vue spécifique de l'UI.
Caractéristiques :
- Séparé des entités domain
- Optimisé pour une utilisation spécifique (liste, détail, card, etc.)
- Peut agréger des données de plusieurs entités
- Plain TypeScript interface (pas de validation)
- Nommé avec le suffixe
ReadModel
Template Read Model
// application/read-models/club-detail.read-model.ts
export interface ClubDetailReadModel {
id: string;
name: string;
description: string | null;
createdAt: Date;
// Owner info
owner: {
id: string;
name: string;
email: string;
};
// Subscription info (agrégation)
subscription: {
plan: string;
status: string;
maxTeams: number;
currentTeamsCount: number;
};
// Members count
membersCount: number;
// Teams info
teams: {
id: string;
name: string;
category: string;
}[];
}
// application/read-models/club-list.read-model.ts
export interface ClubListReadModel {
id: string;
name: string;
description: string | null;
membersCount: number;
createdAt: Date;
}
// application/read-models/index.ts
export * from './club-detail.read-model';
export * from './club-list.read-model';
export * from './subscription-status.read-model';
export * from './member-list.read-model';
RĂšgles pour les Read Models
- â Une Read Model par vue UI spĂ©cifique
- â SĂ©lection des champs pertinents uniquement
- â AgrĂ©gation de donnĂ©es de plusieurs entitĂ©s si nĂ©cessaire
- â Types primitifs (string, number, boolean, Date)
- â Nested objects si nĂ©cessaire pour l'UI
- â JAMAIS de mĂ©thodes (pure data)
- â JAMAIS de validation decorators (class-validator)
- â JAMAIS de logique mĂ©tier
đ IntĂ©gration avec les Controllers
Comment utiliser Commands et Queries dans les Controllers
// presentation/controllers/clubs.controller.ts
import { Controller, Post, Get, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { CreateClubCommand } from '../../application/commands/create-club/create-club.command';
import { CreateClubHandler } from '../../application/commands/create-club/create-club.handler';
import { UpdateClubCommand } from '../../application/commands/update-club/update-club.command';
import { UpdateClubHandler } from '../../application/commands/update-club/update-club.handler';
import { GetClubQuery } from '../../application/queries/get-club/get-club.query';
import { GetClubHandler } from '../../application/queries/get-club/get-club.handler';
import { ListClubsQuery } from '../../application/queries/list-clubs/list-clubs.query';
import { ListClubsHandler } from '../../application/queries/list-clubs/list-clubs.handler';
@Controller('clubs')
@UseGuards(JwtAuthGuard)
export class ClubsController {
constructor(
// Inject handlers (NOT use cases)
private readonly createClubHandler: CreateClubHandler,
private readonly updateClubHandler: UpdateClubHandler,
private readonly getClubHandler: GetClubHandler,
private readonly listClubsHandler: ListClubsHandler,
) {}
// Command - Retourne un ID uniquement
@Post()
async create(@Body() command: CreateClubCommand) {
const id = await this.createClubHandler.execute(command);
return { id }; // Payload minimal
}
// Command - Retourne un ID uniquement
@Put(':id')
async update(@Param('id') id: string, @Body() command: UpdateClubCommand) {
const updatedId = await this.updateClubHandler.execute(command);
return { id: updatedId };
}
// Query - Retourne un Read Model
@Get(':id')
async findOne(@Param('id') id: string) {
const query = new GetClubQuery(id);
return this.getClubHandler.execute(query); // Read Model
}
// Query - Retourne une liste de Read Models avec pagination
@Get()
async findAll(@Query() params: any) {
const query = new ListClubsQuery(
params.page,
params.limit,
params.search,
params.userId,
);
return this.listClubsHandler.execute(query); // PaginatedResult<ReadModel>
}
}
RÚgles pour l'intégration Controller
- â Injecter les Handlers (pas les use cases)
- â
Commands retournent
{ id: string } - â Queries retournent Read Models directement
- â Validation automatique via class-validator (NestJS)
- â JAMAIS de logique mĂ©tier dans le controller
- â JAMAIS de mapping manuel (le handler s'en charge)
đ§ Module Configuration
Enregistrer les Handlers comme Providers
// club-management.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
// Controllers
import { ClubsController } from './presentation/controllers/clubs.controller';
// Command Handlers
import { CreateClubHandler } from './application/commands/create-club/create-club.handler';
import { UpdateClubHandler } from './application/commands/update-club/update-club.handler';
import { DeleteClubHandler } from './application/commands/delete-club/delete-club.handler';
// Query Handlers
import { GetClubHandler } from './application/queries/get-club/get-club.handler';
import { ListClubsHandler } from './application/queries/list-clubs/list-clubs.handler';
// Repositories
import { ClubRepository } from './infrastructure/persistence/repositories/club.repository';
import { CLUB_REPOSITORY } from './domain/repositories/club.repository.interface';
@Module({
imports: [PrismaModule],
controllers: [ClubsController],
providers: [
// Repository binding
{
provide: CLUB_REPOSITORY,
useClass: ClubRepository,
},
// Command Handlers
CreateClubHandler,
UpdateClubHandler,
DeleteClubHandler,
// Query Handlers
GetClubHandler,
ListClubsHandler,
],
exports: [
CLUB_REPOSITORY,
],
})
export class ClubManagementModule {}
â Checklist CQRS
Commands
- Command nommé avec un verbe d'action (CreateX, UpdateX, DeleteX)
- DTO validé avec class-validator
- Handler orchestre les entités domain
- Retourne un ID (string) ou void
- Pas de logique métier dans le handler
- Co-localisé avec son handler
Queries
- Query nommée avec intention de lecture (GetX, ListX, SearchX)
- Supporte pagination/filtrage si liste
- Handler transforme Domain Entities â Read Models
- Retourne Read Models (pas les entités brutes)
- Pas de modification d'état
- Co-localisée avec son handler
Read Models
- Interface TypeScript (pas de class)
- Optimisé pour une vue UI spécifique
- Champs pertinents uniquement
- Peut agréger plusieurs entités
- Pas de validation decorators
- Exporté via barrel (index.ts)
Handlers
- Injectent les repository interfaces (pas les implémentations)
- GÚrent les erreurs métier
- Tests unitaires présents
- Un handler par command/query
đ Exemples Concrets du Projet
Bounded Context club-management
Commands :
create-club: Créer un nouveau clubupdate-club: Mettre à jour les informations d'un clubdelete-club: Supprimer un clubsubscribe-to-plan: Souscrire à un plan d'abonnementupgrade-subscription: Upgrader un plan d'abonnementgenerate-invitation: Générer une invitationaccept-invitation: Accepter une invitationremove-member: Retirer un membrechange-club: Changer de club
Queries :
get-club: Récupérer les détails d'un clublist-clubs: Lister les clubs (avec pagination)get-subscription: Récupérer le statut d'abonnementlist-subscription-plans: Lister les plans disponiblesvalidate-invitation: Valider une invitationlist-members: Lister les membres d'un club
Read Models :
ClubDetailReadModel: Vue détaillée d'un clubClubListReadModel: Vue liste des clubsSubscriptionStatusReadModel: Statut d'abonnementMemberListReadModel: Liste des membres
Référence : volley-app-backend/src/club-management/application/
đš Erreurs Courantes Ă Ăviter
-
â Command qui retourne un Read Model
- â
FAIRE : Command retourne
{ id: string }, puis Query sĂ©parĂ©e pour rĂ©cupĂ©rer le Read Model - â NE PAS FAIRE : Command retourne l'entitĂ© complĂšte ou le Read Model
- â
FAIRE : Command retourne
-
â Query qui modifie l'Ă©tat
- â FAIRE : Query lit uniquement, jamais de modification
- â NE PAS FAIRE :
GetAndMarkAsReadQuery(séparé en Query + Command)
-
â Logique mĂ©tier dans le Handler
- â
FAIRE :
club.upgrade(newPlan)(logique dans l'entitĂ©) - â NE PAS FAIRE : Validation mĂ©tier dans le handler
- â
FAIRE :
-
â Handler qui appelle Prisma directement
- â
FAIRE :
await this.clubRepository.create(club) - â NE PAS FAIRE :
await this.prisma.club.create(...)
- â
FAIRE :
-
â Read Model = Domain Entity
- â FAIRE : Read Model optimisĂ© pour l'UI, diffĂ©rent de l'entitĂ©
- â NE PAS FAIRE : Retourner l'entitĂ© domain brute au client
đ Skills ComplĂ©mentaires
Pour aller plus loin :
- ddd-bounded-context : Architecture DDD complĂšte avec bounded contexts
- ddd-testing : Standards de tests pour Commands/Queries/Handlers
- prisma-mapper : Patterns de mappers Domain â Prisma
Rappel : CQRS sĂ©pare les Ăcritures (Commands) des Lectures (Queries) pour une architecture plus claire, scalable et maintenable.
GitHub Repository
Related Skills
content-collections
MetaThis skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.
creating-opencode-plugins
MetaThis skill provides the structure and API specifications for creating OpenCode plugins that hook into 25+ event types like commands, files, and LSP operations. It offers implementation patterns for JavaScript/TypeScript modules that intercept and extend the AI assistant's lifecycle. Use it when you need to build event-driven plugins for monitoring, custom handling, or extending OpenCode's capabilities.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
Algorithmic Art Generation
MetaThis skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.
