AGENT SKILL

volontariapp-api-gateway

Playbook opérationnel pas-à-pas pour exposer ou modifier une route REST Edge sur api-gateway : DTO Swagger, contrôleur REST, guards, token interne propagé en métadonnée gRPC, support HTTP 206 et tests.

适用职业开发工程师
使用场景后端与接口
License 许可
MIT
所属仓库
Volontariapp/meta
最近更新
2026-11-10
Stars
★ 0
Forks
0

01 前言 Front Matter

name
volontariapp-api-gateway
tags
[ "api-gateway", "auth", "grpc", "rest", "swagger", "playbook" ]
type
Playbook
paths
[ "api-gateway/src/**" ]
title
Exposer ou Modifier une Route REST Edge
status
stable
sources
[ { "id": "gw-claude", "title": "Modules de api-gateway (carte : references/routes.md)", "resource": "api-gateway/src/modules" }, { "id": "decorator", "title": "GatewayController", "resource": "api-gateway/src/common/decorators/gateway-controller.decorator.ts" }, { "id": "app-module", "title": "AppModule (GrpcInternalInterceptor, HelperModule)", "resource": "api-gateway/src/app.module.ts" }, { "id": "grpc-module", "title": "GrpcClientModule", "resource": "api-gateway/src/grpc/grpc-client.module.ts" }, { "id": "helper", "title": "TokenHelperController", "resource": "api-gateway/src/modules/helper/controllers/token-helper.controller.ts" }, { "id": "seed", "title": "SystemSeedController", "resource": "api-gateway/src/modules/system/controllers/system-seed.controller.ts" }, { "id": "deploy-gw", "title": "Déploiement api-gateway (NODE_ENV)", "resource": "deploy/apps/base/api-gateway/deployment.yaml" }, { "id": "errors-nest", "title": "FALLBACK_ACTIVATED (PartialContentError 206)", "resource": "npm-packages/packages/errors-nest/src/errors/common.errors.ts" }, { "id": "ws-proxy", "title": "WsProxyMiddleware", "resource": "api-gateway/src/modules/ws-proxy/ws-proxy.middleware.ts" }, { "id": "pagination", "title": "Module de pagination commun (constantes, DTO, decorateurs, ecretage)", "resource": "api-gateway/src/common/dto/request/pagination.request.dto.ts" }, { "id": "check-arch", "title": "yarn check-arch (regles des DTO)", "resource": "api-gateway/scripts/check-dto-architecture.ts" }, { "id": "web-requests", "title": "Types web de requete du classement", "resource": "npm-packages/packages/contracts/src/gateway/user/request/leaderboard.request.ts" } ]
verified
[ { "at": "2026-11-03T20:40:37Z", "by": "claude-code/agent", "digest": "be5ca2862e35d8ad" }, { "at": "2026-11-04T17:55:25Z", "by": "claude-code/agent", "digest": "36e80228c7f25540" }, { "at": "2026-11-08T12:02:21Z", "by": "claude-code/agent", "digest": "6e8237e4a9c0e774" }, { "at": "2026-11-08T12:18:23Z", "by": "claude-code/agent", "digest": "438a549648dfd54b" }, { "at": "2026-11-08T22:50:47Z", "by": "claude-code/agent", "digest": "438a549648dfd54b" } ]
generated
{ "at": "2026-10-06T08:54:04Z", "by": "claude-code/claude-opus-5-5" }
mesh_keys
[ "api-gateway", "gatewaycontroller", "accesstokenguard", "rolesguard", "grpcinternalinterceptor", "internalmetadata", "iscurrentuseroradminguard", "ws-proxy" ]
description
Playbook opérationnel pas-à-pas pour exposer ou modifier une route REST Edge sur api-gateway : DTO Swagger, contrôleur REST, guards, token interne propagé en métadonnée gRPC, support HTTP 206 et tests.

02 资源文件 5

03 SKILL.md 正文

Playbook : Exposer ou Modifier une Route REST Edge (API Gateway)

Dans l'écosystème Volontariapp, api-gateway est l'unique point d'entrée HTTP exposé publiquement (préfixe global /api/v1)[^gw-claude].

L'API Gateway ne contient aucune logique métier : son rôle exclusif consiste à authentifier l'utilisateur, valider le payload REST, relayer la requête au microservice cible via gRPC en y injectant un token interne signé (x-internal-token), et traduire la réponse ou les erreurs en HTTP.

La référence des modules, de la topologie réseau et des risques de sécurité est consignée dans Sécurité & Risques API Gateway.


Trigger

Ce playbook doit être exécuté dès qu'une tâche nécessite :

  • L'ajout d'une nouvelle route REST publique ou authentifiée (GET, POST, PUT, PATCH, DELETE).
  • L'exposition d'un nouveau paramètre de requête ou champ de réponse vers l'application mobile nativapp.
  • L'ajustement des règles d'authentification ou des permissions de rôle sur un endpoint existant.

Étape 0 : Le type Web du contrat doit exister (sinon, npm-packages d'abord)

yarn check-arch impose deux choses[^check-arch] :

  • un DTO de réponse implémente une interface « Web » de @volontariapp/contracts : même forme que le message gRPC, mais avec des Date à la place des Timestamp, dans npm-packages/packages/contracts/src/gateway/<domaine>/response/*.ts ;
  • un DTO de requête implémente lui aussi une interface de @volontariapp/contracts, dans .../gateway/<domaine>/request/*.ts, de la forme extends Partial<XxxQuery>. Quand le REST expose une valeur différente du gRPC (une période en texte 'global' | 'week' | 'month' là où le gRPC porte un enum), le type Web de requête porte la valeur REST et le DTO la convertit dans toQuery()[^web-requests].

Ces interfaces sont écrites à la main (seuls les messages gRPC sont générés). Si l'un des deux types manque, il faut une PR npm-packages (ajout seul, changeset minor), sa publication, puis le déverrouillage (règle du STOP) avant d'écrire le DTO. Poser le type de requête et le type de réponse dans la même PR : le classement les a livrés en deux fois (contracts 4.11.0 puis 4.12.0), ce qui a coûté un cycle de publication de plus. Modèle de réponse : BadgeProgressWeb extends Omit<BadgeProgress, 'awardedAt'> { awardedAt?: Date } et GetMyBadgeProgressWebResponse (npm-packages#300).

Étape 1 : Spécification du DTO Swagger & Validation

Emplacement : src/modules/<domaine>/dto/request/<nom>.request.dto.ts.

Chaque DTO de requête doit :

  1. Documenter les champs pour OpenAPI/Swagger avec @ApiProperty() ou @ApiPropertyOptional().
  2. Appliquer des décorateurs class-validator stricts.
  3. Implémenter la méthode adaptatrice toCommand(): CommandType ou toQuery(): QueryType pour convertir le format REST en contrat gRPC.
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsUUID, IsOptional } from 'class-validator';
import type { CreatePostCommand } from '@volontariapp/contracts-nest';

export class CreatePostRequestDTO {
  @ApiProperty({ description: 'Titre de la publication', example: 'Mission Maraude Paris' })
  @IsString()
  @IsNotEmpty()
  title!: string;

  @ApiProperty({ description: 'Contenu textuel', example: 'Besoin de 5 bénévoles ce samedi' })
  @IsString()
  @IsNotEmpty()
  content!: string;

  @ApiPropertyOptional({ description: 'Identifiant du fichier image déjà confirmé' })
  @IsUUID()
  @IsOptional()
  coverFileId?: string;

  toCommand(): CreatePostCommand {
    return {
      title: this.title,
      content: this.content,
      coverFileId: this.coverFileId,
    };
  }
}

Étape 2 : Contrôleur REST NestJS & Règles de Sécurité

Emplacement : src/modules/<domaine>/controllers/commands/ (écriture) ou queries/ (lecture).

import { Controller, Post, Body, Req, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Request } from 'express';
import { firstValueFrom } from 'rxjs';
import { GatewayController } from '../../../../common/decorators/gateway-controller.decorator.js';
import { CurrentUser, type AuthUser } from '@volontariapp/auth';
import { BasePostGrpcController } from '../base-post-grpc.controller.js';
import { CreatePostRequestDTO } from '../../dto/request/create-post.request.dto.js';
import { PostResponseDTO } from '../../dto/response/post.response.dto.js';

@GatewayController('Posts')
export class PostCommandController extends BasePostGrpcController {

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({ summary: 'Créer une nouvelle publication' })
  @ApiResponse({ status: 201, type: PostResponseDTO })
  async createPost(
    @Body() dto: CreatePostRequestDTO,
    @CurrentUser() user: AuthUser,
    @Req() req: Request,
  ): Promise<PostResponseDTO> {
    const command = dto.toCommand();
    
    // Appel gRPC avec injection obligatoire de req['internalMetadata']
    const result = await firstValueFrom(
      this.postCommandService.createPost(command, req['internalMetadata']),
    );

    return PostResponseDTO.fromGrpc(result);
  }
}

Règles de Sécurité Inviolables :

  • Administration : Pour toute route d'administration, utiliser impérativement @GatewayController('Tag', { admin: true })[^decorator]. Sans { admin: true }, le garde RolesGuard n'est pas monté, rendant @Roles(UserRoles.ADMIN) totalement inopérant[^seed].
  • Routes Publiques : Ajouter @Public() uniquement pour les endpoints ouverts sans authentification (/login, /refresh)[^helper].
  • Identité du demandeur : Pour les routes de profil ou d'action personnelle (/me), extraire l'utilisateur exclusivement via @CurrentUser()[^app-module]. Ne jamais accepter de userId dans le body ou les paramètres d'URL pour le demandeur connecté.
  • Ressources scopées par utilisateur : Utiliser IsCurrentUserOrAdminGuard pour les routes ciblant :userId.

Conflit de routes. Un chemin sous un préfixe qui porte déjà un GET :id (badges/:id, users/:id) peut être capturé par lui. Le placer dans un contrôleur dédié monté sur un préfixe sans ambiguïté (@Controller('users/me/badges') pour GET /users/me/badges/progress) et écrire un test d'intégration de routage (Test.createTestingModule avec les contrôleurs voisins, dans l'ordre du module) qui vérifie que la route atteint le bon contrôleur.

Étape 3 : Câblage du Client gRPC

  1. Le contrôleur hérite du contrôleur de base du domaine (Base<Domaine>GrpcController)[^grpc-module].
  2. Lors de onModuleInit, le contrôleur résout le client typé gRPC via client.getService<XServiceClient>(X_SERVICE_NAME).
  3. Si un nouveau package de microservice doit être raccordé (ex. ms-storage), l'enregistrer dans GrpcClientModule via getGrpcOptions(GRPC_MICROSERVICES.X, url).

Étape 4 : Propagation Obligatoire du Token Interne

Tout microservice en aval exige un x-internal-token signé pour autoriser la requête.

  • L'intercepteur global GrpcInternalInterceptor génère et attache automatiquement un objet grpc.Metadata sur la requête Express : req['internalMetadata'][^app-module].
  • Règle absolue : Vous devez obligatoirement passer req['internalMetadata'] comme second argument de chaque appel gRPC :
    await firstValueFrom(this.service.someRpc(command, req['internalMetadata']));
    
  • Omettre cet argument provoque un rejet immédiat du microservice avec le statut UNAUTHENTICATED.

Étape 5 : Mapping des Réponses et Gestion du HTTP 206

  1. Mapping d'Erreurs : Les erreurs BaseApiError renvoyées par les microservices portent statusCode et grpcCode. L'API Gateway les retranscrit avec le statut HTTP d'origine.
  2. Support HTTP 206 FALLBACK_ACTIVATED :
    • Lorsqu'un microservice subit une défaillance de dépendance non bloquante, il déclenche un job de fallback et retourne FALLBACK_ACTIVATED[^errors-nest].
    • L'API Gateway doit renvoyer ce résultat avec le code HTTP 206 (Partial Content) :
    @ApiResponse({ status: 206, description: 'Traitement asynchrone différé (fallback activé)' })
    
    • Le client mobile nativapp sait qu'il doit écouter la notification de finalisation via WebSocket.

Étape 6 : Tests & Validation de l'Endpoint

  1. Tests unitaires de Contrôleur :
    • Mocker le client gRPC (ex: MockUserClientGrpc).
    • Mocker l'objet Request avec req['internalMetadata'] = new Metadata().
    • Vérifier que le DTO appelle bien toCommand() et que le retour est conforme.
  2. Validation Swagger :
    • Démarrer l'API Gateway ou inspecter la spec générée : vérifier que le tag, les statuts 200/201/206/400/401/403 et les schémas DTO sont intégralement documentés.

Pagination des routes de liste

Toute route de liste utilise le module commun src/common/pagination : ne jamais redeclarer page, limit ni un plafond dans un DTO[^pagination].

  • Constantes : DEFAULT_PAGE = 1 (base 1), DEFAULT_LIMIT = 10, MAX_LIMIT = 100, LEGACY_MAX_LIMIT = 500.
  • DTO imbrique pagination[page|limit] : PaginationRequestDTO (plafond 100), LegacyPaginationRequestDTO (plafond 500), ou createPaginationRequestDTO(maxLimit) pour un plafond propre. Le champ du DTO de requete se declare avec @NestedPagination(LegacyPaginationRequestDTO) ou son pendant standard.
  • DTO de query plate ?page=&limit= : FlatPaginationQueryDTO (plafond 100) ou createFlatPaginationQueryDTO(maxLimit).
  • Decorateurs : @PaginationPage() (entier >= 1, sinon 400) et @PaginationLimit(maxLimit = MAX_LIMIT).
  • Ecretage : clampPaginationLimit(limit, maxLimit) ramene une limite trop grande au plafond (Math.min). Une limite superieure au plafond est ecretee, jamais rejetee ; une limite invalide (non entiere, < 1) reste un 400.
  • Exception legacy (500) : seulement GET /api/v1/social/likes (GetMyLikesRequestDTO) et GET /api/v1/users admin (ListUsersRequestDTO), car les anciens builds de nativapp appellent avec limit=500. Ne pas etendre cette exception a une autre route.
  • Lecture par lot : POST /api/v1/social/likes-summary (GetPostsLikeSummaryRequestDTO) prend 1 a 100 postIds uniques (ArrayMinSize(1), ArrayMaxSize(MAX_LIKE_SUMMARY_POST_IDS), ArrayUnique) et repond 200 : c'est une lecture exposee en POST pour eviter une URL de 100 identifiants. ms-social reste l'autorite sur le plafond.
  • Lire tout un ensemble d'ids chez un ms (appel interne, pas une route) : fetchAllPages(fetchPage) de src/common/pagination (api-gateway#90), jamais un seul appel avec limit: 1000. Il lit par pages de INTERNAL_PAGE_SIZE (= MAX_LIMIT, 100) et s'arrete sur page vide, page courte, totalPages atteint ou INTERNAL_MAX_PAGES (100 pages, soit 10 000 ids au plus : les ids deja lus sont alors renvoyes sans erreur). Une erreur de fetchPage est propagee telle quelle. Utilise pour les ids d'evenements recommandes par ms-social et pour les ensembles de blocage du ranking.

[^pagination]: Module de pagination commun (constantes, DTO, decorateurs, ecretage)


Checklist d'Assurance Qualité & Critères d'Arrêt

Avant de déclarer la tâche terminée :

  • [ ] Le DTO de requête implémente les décorateurs Swagger et class-validator.
  • [ ] Le DTO possède sa méthode adaptatrice toCommand() ou toQuery().
  • [ ] @GatewayController(tag, { admin: true }) est posé sur toute route d'administration.
  • [ ] req['internalMetadata'] est passé en second argument de chaque appel gRPC.
  • [ ] Aucun userId n'est extrait du payload sur les routes « me ».
  • [ ] Les réponses asynchrones avec fallback documentent le statut HTTP 206 (FALLBACK_ACTIVATED).
  • [ ] yarn test passe avec succès sur api-gateway.
  • [ ] yarn check-arch passe (un fichier par DTO, requete avec implements + @volontariapp/contracts + toCommand()/toQuery(), DTO imbriques dans dto/common/)[^check-arch].
  • [ ] yarn tsc et yarn lint ne produisent aucune erreur.

[^gw-claude]: Modules de api-gateway (carte : references/routes.md) [^decorator]: GatewayController [^app-module]: AppModule (GrpcInternalInterceptor, HelperModule) [^grpc-module]: GrpcClientModule [^helper]: TokenHelperController [^seed]: SystemSeedController [^deploy-gw]: Déploiement api-gateway (NODE_ENV) [^errors-nest]: FALLBACK_ACTIVATED (PartialContentError 206) [^ws-proxy]: WsProxyMiddleware [^check-arch]: yarn check-arch (regles des DTO) [^web-requests]: Types web de requete du classement

04 同仓库更多 Skills

Volontariapp
Volontariapp/meta

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.

★ 0
Volontariapp
Volontariapp/meta

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.

律师与法务
★ 0
Volontariapp
Volontariapp/meta

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.

Skill与智能体开发
★ 0
Volontariapp
Volontariapp/meta

volontariapp-implement-async-event-flow

Guide pas-à-pas pour concevoir et implémenter un flux asynchrone complet (messaging -> outbox -> post-processors -> ws-service -> client).

前端开发
★ 0
Volontariapp
Volontariapp/meta

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.

资料检索与研究
★ 0
Volontariapp
Volontariapp/meta

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.

设计师
★ 0