← Back to Skills

CQRS Command Query Generator

majiayu000
Updated Today
1 views
58
9
58
View on GitHub
Metadesign

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 CommandRecommended
/plugin add https://github.com/majiayu000/claude-skill-registry
Git CloneAlternative
git clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/CQRS Command Query Generator

Copy 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 :

  1. 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)
  2. 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 club
  • update-club : Mettre Ă  jour les informations d'un club
  • delete-club : Supprimer un club
  • subscribe-to-plan : Souscrire Ă  un plan d'abonnement
  • upgrade-subscription : Upgrader un plan d'abonnement
  • generate-invitation : GĂ©nĂ©rer une invitation
  • accept-invitation : Accepter une invitation
  • remove-member : Retirer un membre
  • change-club : Changer de club

Queries :

  • get-club : RĂ©cupĂ©rer les dĂ©tails d'un club
  • list-clubs : Lister les clubs (avec pagination)
  • get-subscription : RĂ©cupĂ©rer le statut d'abonnement
  • list-subscription-plans : Lister les plans disponibles
  • validate-invitation : Valider une invitation
  • list-members : Lister les membres d'un club

Read Models :

  • ClubDetailReadModel : Vue dĂ©taillĂ©e d'un club
  • ClubListReadModel : Vue liste des clubs
  • SubscriptionStatusReadModel : Statut d'abonnement
  • MemberListReadModel : Liste des membres

Référence : volley-app-backend/src/club-management/application/

🚹 Erreurs Courantes à Éviter

  1. ❌ 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
  2. ❌ Query qui modifie l'Ă©tat

    • ✅ FAIRE : Query lit uniquement, jamais de modification
    • ❌ NE PAS FAIRE : GetAndMarkAsReadQuery (sĂ©parĂ© en Query + Command)
  3. ❌ Logique mĂ©tier dans le Handler

    • ✅ FAIRE : club.upgrade(newPlan) (logique dans l'entitĂ©)
    • ❌ NE PAS FAIRE : Validation mĂ©tier dans le handler
  4. ❌ Handler qui appelle Prisma directement

    • ✅ FAIRE : await this.clubRepository.create(club)
    • ❌ NE PAS FAIRE : await this.prisma.club.create(...)
  5. ❌ 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

majiayu000/claude-skill-registry
Path: skills/cqrs-command-query

Related Skills

content-collections

Meta

This 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.

View skill

creating-opencode-plugins

Meta

This 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.

View skill

langchain

Meta

LangChain 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.

View skill

Algorithmic Art Generation

Meta

This 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.

View skill