volontariapp-grpc-microservice
Playbook opérationnel pas-à-pas pour implémenter ou modifier un RPC dans un microservice gRPC (ms-user, ms-event, ms-post, ms-social) : contrat, DTO, contrôleur CQRS, garde interne, fallback, mappers et tests.
- License 许可
- MIT
- 最近更新
- 2026-11-10
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- name
- volontariapp-grpc-microservice
- tags
- [ "microservice", "grpc", "nestjs", "ddd", "cqrs", "playbook" ]
- type
- Playbook
- paths
- [ "ms-user/src/**", "ms-event/src/**", "ms-post/src/**", "ms-social/src/**", "scripts/cli/commands/sync-migrations.mjs" ]
- title
- Implémenter ou Modifier un RPC gRPC
- status
- stable
- sources
- [ { "id": "agent-md", "title": "AGENT.md (structure de module, DTO, contrôleurs, tests)", "resource": "AGENT.md" }, { "id": "main-user", "title": "Bootstrap hybride (gRPC + HTTP /health)", "resource": "ms-user/src/main.ts" }, { "id": "app-event", "title": "AppModule ms-event (GrpcValidationPipe, APP_GUARD)", "resource": "ms-event/src/app.module.ts" }, { "id": "base-cmd", "title": "BaseCommandController.withFallback", "resource": "ms-event/src/modules/event/controllers/commands/base.command.controller.ts" }, { "id": "sync-mig", "title": "sync-migrations.mjs", "resource": "scripts/cli/commands/sync-migrations.mjs" }, { "id": "validation", "title": "GrpcValidationPipe", "resource": "npm-packages/packages/validation-nest" } ]
- verified
- [ { "at": "2026-10-30T12:29:16Z", "by": "claude-code/agent", "digest": "37f4a83af8a6d3d1" }, { "at": "2026-11-03T20:31:03Z", "by": "claude-code/agent", "digest": "79cb461c249d3c2e" }, { "at": "2026-11-04T17:53:23Z", "by": "claude-code/agent", "digest": "a4e563bea3a17189" }, { "at": "2026-11-06T14:04:01Z", "by": "claude-code/agent", "digest": "90966abbcd8b04bd" }, { "at": "2026-11-08T12:02:22Z", "by": "claude-code/agent", "digest": "e1f929df232e29f5" } ]
- generated
- { "at": "2026-10-06T08:54:04Z", "by": "claude-code/claude-opus-5-5" }
- mesh_keys
- [ "ms-user", "ms-event", "ms-post", "ms-social", "basecommandcontroller", "grpcvalidationpipe", "grpcinternalguard", "transformer" ]
- description
- Playbook opérationnel pas-à-pas pour implémenter ou modifier un RPC dans un microservice gRPC (ms-user, ms-event, ms-post, ms-social) : contrat, DTO, contrôleur CQRS, garde interne, fallback, mappers et tests.
02 资源文件 9
- 参考文档index.md2.0 KB
- 参考文档log.md9.7 KB
- 参考文档references/lessons.md10.9 KB
- 参考文档references/microservice-architecture.md4.4 KB
- 参考文档references/ms-event.md3.8 KB
- 参考文档references/ms-post.md2.6 KB
- 参考文档references/ms-social.md2.7 KB
- 参考文档references/ms-user.md4.2 KB
- 参考文档references/services.md4.1 KB
03 SKILL.md 正文
Playbook : Implémenter ou Modifier un RPC gRPC dans un Microservice
Dans l'architecture Volontariapp, un microservice ms-* est une coquille NestJS purement dédiée à l'exposition gRPC (port 3000) et HTTP /health[^main-user] : il valide les messages entrants, applique la garde du token interne de sécurité, et délègue l'exécution métier au package de domaine partagé @volontariapp/domain-<domaine>.
La documentation d'architecture détaillée et les conventions structurelles sont disponibles dans Architecture Microservice et AGENT.md^agent-md.
Trigger
Ce playbook doit être exécuté dès qu'une tâche nécessite :
- L'ajout d'une nouvelle méthode RPC
rpc Method(Command) returns (Response)dans un microservice existant. - La modification d'un endpoint gRPC existant (nouveau champ reçu ou retourné, ajustement de validation).
- L'ajout d'un appel gRPC sortant vers un autre microservice du mesh.
Étape 1 : Vérification des Contrats Amont & Règle du STOP
Avant d'écrire la moindre ligne de code dans ms-* :
- Localiser la méthode dans le graphe gRPC :
analyze_grpc({ "target": "<MethodName>" }) - Contrôler la présence du contrat dans
@volontariapp/contracts-nest:- L'interface de commande/requête, l'interface de réponse et les constantes de service (
GRPC_SERVICES,<SERVICE>_METHODS) doivent être exportées par le package publié.
- L'interface de commande/requête, l'interface de réponse et les constantes de service (
[!CAUTION]
🛑 RÈGLE CRITIQUE DU STOP IMMÉDIAT
Si le RPC, le message ou le champ n'existe pas encore dans les contrats TypeScript :
- INTERDICTION FORMELLE de créer des interfaces locales temporaires ou d'injecter des types arbitraires.
- Dérouler d'abord le playbook
volontariapp-proto-contract-evolutiondansproto-registry/.- Attendre la publication de
@volontariapp/contracts-nestpar la CI.- Mettre à jour les dépendances du microservice :
yarn up @volontariapp/contracts-nest.
Étape 2 : Création des DTOs de Requête (Validation)
Emplacement : src/modules/<domaine>/dto/request/{command,query}/.
Chaque DTO de commande ou requête doit impérativement implémenter l'interface TypeScript du contrat gRPC et définir des règles class-validator strictes :
import { IsString, IsNotEmpty, IsUUID, IsEnum, IsOptional } from 'class-validator';
import type { CreateEventCommand, EventType } from '@volontariapp/contracts-nest';
export class CreateEventCommandDTO implements CreateEventCommand {
@IsString()
@IsNotEmpty()
title!: string;
@IsEnum(EventType)
type!: EventType;
@IsUUID()
@IsOptional()
coverFileId?: string;
}
- Règles de validation automatique :
GrpcValidationPipeest configuré enAPP_PIPEglobal avecenumMaps[^app-event][^validation]. Il convertit les chaînes vides enundefined, les entiers Int64 ennumber, et les noms d'enums sous forme de string en valeurs numériques conformes Protobuf. - Défense en profondeur : Utiliser l'assertion d'assignation définie
!pour les champs obligatoires du contrat (aucunany). - Pagination des requêtes de liste : ne jamais déclarer
paginationavec le seul@IsOptional()(aucune valeur n'est alors validée). Motif commun :@IsOptional() @ValidateNested() @Type(() => PaginationRequestDTO) pagination: PaginationRequestDTO | undefined;, avecPaginationRequestDTOdanssrc/common/dto/pagination.dto.ts(page:@IsInt @Min(1),limit:@IsInt @Min(1) @Max(MAX_PAGINATION_LIMIT)). Le plafondMAX_PAGINATION_LIMITdépend du service (relevé le 2026-11-04) : 100 dansms-post(#69) etms-event(#83), qui l'ont abaissé de 1000 à 100, et 1000 dansms-useretms-social(la gateway écrête à 100 sur ses routes, et lit les ensembles d'ids par pages de 100 avecfetchAllPagesdepuis api-gateway#90, au lieu d'un appellimit: 1000). Ne pas supposer 1000 : lire la constante du service. Pagination absente : défaut page 1 / limit 10 dans le contrôleur (DEFAULT_PAGE,DEFAULT_LIMIT), jamais une lecture de toute la table. Listes d'ids en entrée :@IsArray @ArrayMaxSize(100)(1000 pourids/excludedIdsdeSearchEvents, que la gateway remplit avec les événements recommandés). - Lectures par lot : pas de
Promise.all(ids.map(findById)). Utiliser lefindByIdsdu domaine (une requête, 100 ids maximum, tronqué en silence au-delà donc découper par lots de 100) puis remettre le résultat dans l'ordre des ids d'entrée, car cet ordre vient de ms-social pour les listes de profils et d'événements. Exemples :ms-user/src/modules/user/helpers/find-active-users-in-order.ts,ms-event/src/modules/event/utils/events-by-ids.util.ts. Côté domaine, filtrer en SQL avantlimit/offset(UserService.findPage) et toujours trier avec un départage parid(offset instable sinon).
Étape 3 : Câblage Métier dans @volontariapp/domain-*
Les microservices ms-* n'ont aucune logique métier propre :
- Les entités TypeORM, modèles, règles de gestion et repositories résident dans le package partagé
@volontariapp/domain-<domaine>. - Si la nouvelle méthode RPC requiert une nouvelle règle de calcul, une transaction ou une transition d'état :
- Modifier d'abord
npm-packages/packages/domain-<domaine>/. - Appliquer le playbook
volontariapp-shared-npm-package-changeavec bump changeset et publication snapshot. - Lancer
yarn up @volontariapp/domain-<domaine>dans le microservice consommateur.
- Modifier d'abord
- Injecter le service de domaine comme provider dans
<Domaine>Module. - Cas d'une lecture qui agrège plusieurs tables avec un catalogue partagé (progression des badges) : un service local au microservice,
src/modules/<domaine>/services/<nom>.service.ts, qui injecte les repositories TypeORM par@InjectRepository(leDatabaseModuleles expose en global) et délègue le calcul à une fonction pure de@volontariapp/shared. Précédent :BadgeProgressServicedems-user(computeBadgeProgress). Les mocks des repositories vont dans un fichier*.mock.tsséparé.
Étape 4 : Implémentation du Controller gRPC (CQRS)
Les contrôleurs sont strictement scindés selon le pattern CQRS :
- Écriture :
src/modules/<domaine>/controllers/commands/<entite>.command.controller.ts - Lecture :
src/modules/<domaine>/controllers/queries/<entite>.query.controller.ts
Contrôleur de Commande (avec Fallback éventuel) :
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { Logger } from '@volontariapp/logger';
import { GRPC_SERVICES, EVENT_COMMAND_METHODS } from '@volontariapp/contracts-nest';
import { CurrentUser, type AuthUser } from '@volontariapp/auth';
import { BaseCommandController } from './base.command.controller.js';
import { CreateEventCommandDTO } from '../../dto/request/command/create-event.command.dto.js';
import { CreateEventResponseDTO } from '../../dto/response/create-event.response.dto.js';
import { EventResponseMapper } from '../../mappers/event-response.mapper.js';
import { EventDomainService } from '@volontariapp/domain-event';
@Controller()
export class EventCommandController extends BaseCommandController {
private readonly logger = new Logger({ context: EventCommandController.name });
constructor(private readonly eventService: EventDomainService) {
super();
}
@GrpcMethod(GRPC_SERVICES.EVENT_COMMAND_SERVICE, EVENT_COMMAND_METHODS.CREATE_EVENT)
async createEvent(
data: CreateEventCommandDTO,
@CurrentUser() user: AuthUser,
): Promise<CreateEventResponseDTO> {
this.logger.info(`gRPC CreateEvent: ${data.title} par user ${user.id}`);
// Exécution avec fallback pour absorption des pannes de dépendances
const entity = await this.withFallback(
JobMessagingType.FALLBACK_CREATE_EVENT,
user.id,
data,
async () => this.eventService.createEvent(user.id, data),
);
return EventResponseMapper.toCreateResponseDTO(entity);
}
}
- Identification de l'acteur : L'identité provient exclusivement du décorateur
@CurrentUser()injecté parGrpcInternalGuarddepuis l'INTERNAL_TOKEN. Ne jamais lire l'acteur dans le corps du message DTO[^app-event]. - Gestion
withFallback: Héritée deBaseCommandController[^base-cmd], cette méthode ne déclenche aucun job en cas d'erreur client (400, 404, 409, et aussi 403 dansms-event; vérifier les autresms-*avant de supposer le même comportement). Sur panne système ou dépendance injoignable, elle insère un job dansjobs_outboxet renvoie la réponse typéeFALLBACK_ACTIVATED(traduite en HTTP 206 par l'API Gateway).
Pièges du contrôleur de lecture :
- Constante de méthode manquante.
BADGE_QUERY_METHODS,USER_QUERY_METHODS... danscontracts-nest/src/<d>/grpc-helpers.tssont écrites à la main : un nouveau RPC n'y figure pas automatiquement. Utiliser le littéral du.proto:@GrpcMethod(BADGE_SERVICE_NAME, 'GetMyBadgeProgress')(même approche que'GetUsersByIds'), ou ajouter la constante dansnpm-packagessi plusieurs consommateurs la veulent. - Route « me ». Aucun identifiant dans le message :
@UseGuards(GrpcInternalGuard)et@CurrentUser() user: AuthUser, commegetUser. UnuserIdfourni par le client ne doit jamais être lu.
Étape 5 : Mappers & DTOs de Réponse
- Définir le DTO de réponse sous
src/modules/<domaine>/dto/response/<entite>.response.dto.tsen implémentant l'interface du contrat (ex.CreateEventResponse). - Implémenter la transformation dans
src/modules/<domaine>/mappers/<domaine>-response.mapper.ts:- Conversion des dates en chaînes ISO 8601 UTC ou timestamps conformes au
.proto. - Transformation des enums numériques et normalisation des relations.
- Ne jamais exposer directement l'entité TypeORM brute sur le bus gRPC.
- Conversion des dates en chaînes ISO 8601 UTC ou timestamps conformes au
Étape 6 : Stratégie de Tests Obligatoires
Chaque nouveau RPC ou modification doit être couvert par des tests unitaires stricts :
- Isolation des Mocks & Factories :
- Mocks obligatoires dans un fichier dédié :
src/modules/<d>/__tests__/<nom>.mock.ts. - Données de test générées par factories dédiées :
src/modules/<d>/__tests__/<nom>.factory.ts. - Interdiction formelle de déclarer des objets
as unknown as Typeouanyinline dans les tests.
- Mocks obligatoires dans un fichier dédié :
- Utilisation rigoureuse de Spies :
- Toujours espionner les appels de dépendances avec
jest.spyOn(). - Nettoyage systématique dans
afterEach(() => { jest.restoreAllMocks(); }).
- Toujours espionner les appels de dépendances avec
- Cas de test obligatoires :
- Cas nominal : renvoi du DTO de réponse avec mapping exact.
- Cas d'erreur métier : validation des exceptions typées (
@volontariapp/errorsou@volontariapp/errors-nest). - Cas de refus d'authentification : rejet si
x-internal-tokenest manquant.
Checklist d'Assurance Qualité & Critères d'Arrêt
Avant de déclarer la tâche terminée :
- [ ] Le RPC et ses DTOs proviennent exclusivement de
@volontariapp/contracts-nest(aucun type inventé). - [ ] Aucun bout de logique métier n'a été inséré dans
src/(délégué àdomain-<domaine>). - [ ] Aucun cast
as unknown as Typenianyn'est présent. - [ ] L'acteur est extrait via
@CurrentUser(), jamais depuis le body. - [ ] Les mappers transforment proprement les entités en DTOs gRPC.
- [ ]
yarn testpasse avec succès sur le microservice. - [ ]
yarn tscetyarn lintne produisent aucune erreur. - [ ] En cas de migration de base de données associée : exécuter
yarn meta sync-migrationssi commune[^sync-mig].
[^main-user]: Bootstrap hybride (gRPC + HTTP /health) [^app-event]: AppModule ms-event (GrpcValidationPipe, APP_GUARD) [^validation]: GrpcValidationPipe [^base-cmd]: BaseCommandController.withFallback [^sync-mig]: scripts/cli/commands/sync-migrations.mjs
04 同仓库更多 Skills
volontariapp-user-account-deletion
Suppression de compte en deux temps (PENDING_DELETION): requestDeletionWithUserDeleted, finalizeDeletion, fan-out de user.deleted vers les domaines et gather ws. A utiliser pour tout changement de domain-user touchant la suppression, le statut ou le cycle de vie d'un user.
volontariapp-proto-contract-evolution
Modifier un contrat Protobuf de proto-registry (champ, message, enum, RPC) sans casser la compatibilité wire, puis suivre la cascade proto-registry, PR automatique dans npm-packages, publication, consommateurs. À utiliser avant toute édition d'un .proto.
volontariapp-meta-scripts
Utiliser et faire evoluer la CLI Node de meta (setup, doctor, migrate, certs, repos, bump) et le manifeste repos.json, source unique des repos et unites du workspace.
volontariapp-implement-async-event-flow
Guide pas-à-pas pour concevoir et implémenter un flux asynchrone complet (messaging -> outbox -> post-processors -> ws-service -> client).
research
Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.
prototype
Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.