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
- 最近更新
- 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
- 参考文档index.md1.0 KB
- 参考文档log.md7.5 KB
- 参考文档references/lessons.md20.8 KB
- 参考文档references/routes.md5.0 KB
- 参考文档references/security-and-risks.md4.5 KB
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 desDateà la place desTimestamp, dansnpm-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 formeextends 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 danstoQuery()[^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 :
- Documenter les champs pour OpenAPI/Swagger avec
@ApiProperty()ou@ApiPropertyOptional(). - Appliquer des décorateurs
class-validatorstricts. - Implémenter la méthode adaptatrice
toCommand(): CommandTypeoutoQuery(): QueryTypepour 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 gardeRolesGuardn'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 deuserIddans le body ou les paramètres d'URL pour le demandeur connecté. - Ressources scopées par utilisateur : Utiliser
IsCurrentUserOrAdminGuardpour 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
- Le contrôleur hérite du contrôleur de base du domaine (
Base<Domaine>GrpcController)[^grpc-module]. - Lors de
onModuleInit, le contrôleur résout le client typé gRPC viaclient.getService<XServiceClient>(X_SERVICE_NAME). - Si un nouveau package de microservice doit être raccordé (ex.
ms-storage), l'enregistrer dansGrpcClientModuleviagetGrpcOptions(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
GrpcInternalInterceptorgénère et attache automatiquement un objetgrpc.Metadatasur 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
- Mapping d'Erreurs : Les erreurs
BaseApiErrorrenvoyées par les microservices portentstatusCodeetgrpcCode. L'API Gateway les retranscrit avec le statut HTTP d'origine. - 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
nativappsait qu'il doit écouter la notification de finalisation via WebSocket.
- Lorsqu'un microservice subit une défaillance de dépendance non bloquante, il déclenche un job de fallback et retourne
Étape 6 : Tests & Validation de l'Endpoint
- Tests unitaires de Contrôleur :
- Mocker le client gRPC (ex:
MockUserClientGrpc). - Mocker l'objet
Requestavecreq['internalMetadata'] = new Metadata(). - Vérifier que le DTO appelle bien
toCommand()et que le retour est conforme.
- Mocker le client gRPC (ex:
- 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), oucreatePaginationRequestDTO(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) oucreateFlatPaginationQueryDTO(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) etGET /api/v1/usersadmin (ListUsersRequestDTO), car les anciens builds de nativapp appellent aveclimit=500. Ne pas etendre cette exception a une autre route. - Lecture par lot :
POST /api/v1/social/likes-summary(GetPostsLikeSummaryRequestDTO) prend 1 a 100postIdsuniques (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)desrc/common/pagination(api-gateway#90), jamais un seul appel aveclimit: 1000. Il lit par pages deINTERNAL_PAGE_SIZE(=MAX_LIMIT, 100) et s'arrete sur page vide, page courte,totalPagesatteint ouINTERNAL_MAX_PAGES(100 pages, soit 10 000 ids au plus : les ids deja lus sont alors renvoyes sans erreur). Une erreur defetchPageest 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()outoQuery(). - [ ]
@GatewayController(tag, { admin: true })est posé sur toute route d'administration. - [ ]
req['internalMetadata']est passé en second argument de chaque appel gRPC. - [ ] Aucun
userIdn'est extrait du payload sur les routes « me ». - [ ] Les réponses asynchrones avec fallback documentent le statut HTTP 206 (
FALLBACK_ACTIVATED). - [ ]
yarn testpasse avec succès surapi-gateway. - [ ]
yarn check-archpasse (un fichier par DTO, requete avecimplements+@volontariapp/contracts+toCommand()/toQuery(), DTO imbriques dansdto/common/)[^check-arch]. - [ ]
yarn tscetyarn lintne 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-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.