Dans une application NestJS sécurisée par JWT, l'utilisateur authentifié est généralement attaché à l'objet request par un guard (JwtAuthGuard dans notre cas). Pour y accéder dans un contrôleur, on utilise traditionnellement @Request() req puis req.user. Cette approche fonctionne, mais elle est verbeuse, peu typée et casse la lisibilité du code. Heureusement, NestJS offre une solution élégante : créer un décorateur de paramètre personnalisé @CurrentUser().
Dans ce tutoriel, nous allons construire pas à pas un décorateur réutilisable, le typer correctement avec TypeScript, et voir comment l'utiliser efficacement dans un projet réel.
Le problème : un accès verbeux et peu typé à l'utilisateur
Voici un extrait classique de contrôleur d'authentification :
@Get('me')
@ApiBearerAuth('JWT-auth')
async getProfile(@Request() req): Promise<ApiResponseDto<any>> {
return ApiResponseDto.success(req.user, 'User retrieved successfully');
}Plusieurs problèmes apparaissent :
- Le paramètre
reqn'est pas typé : aucune autocomplétion surreq.user. - Le contrôleur dépend de l'objet HTTP brut, ce qui complique les tests unitaires.
- Le code est répétitif : à chaque méthode, il faut écrire
const user = req.user. - La sémantique est faible : la signature de la méthode ne dit pas clairement qu'elle dépend d'un utilisateur connecté.
L'objectif est d'arriver à une signature limpide :
@Get('me')
async getProfile(@CurrentUser() user: User) {
return ApiResponseDto.success(user, 'User retrieved successfully');
}Prérequis
- Un projet NestJS 10+ fonctionnel.
- Un système d'authentification JWT déjà en place avec un
JwtAuthGuardqui injecte l'utilisateur dansrequest.user. - Une entité
User(ici basée sur TypeORM) — dans notre exemple, l'utilisateur possède unid(UUID), unemail, unrole(enumUserRole), etc.
Comprendre createParamDecorator
NestJS expose une factory utilitaire, createParamDecorator, qui permet de créer un décorateur de paramètre. Sa signature est simple : on lui passe une fonction qui reçoit deux arguments — data (la valeur passée entre parenthèses lors de l'utilisation, ex. @CurrentUser('id')) et ctx, un ExecutionContext.
L'ExecutionContext est une abstraction puissante : elle permet à NestJS de gérer indifféremment HTTP, WebSocket ou GraphQL. Pour un contrôleur HTTP classique, on bascule vers le contexte HTTP avec ctx.switchToHttp() puis on récupère la requête avec .getRequest(). C'est exactement ce que fait JwtAuthGuard en interne.
Étape 1 : Créer le décorateur @CurrentUser()
Créons un fichier src/common/decorators/current-user.decorator.ts :
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '../../database/entities/user.entity';
/**
* Décorateur de paramètre permettant d'extraire l'utilisateur authentifié
* depuis la requête HTTP. L'utilisateur est injecté par le JwtAuthGuard.
*
* Usage :
* @CurrentUser() user: User => retourne l'utilisateur complet
* @CurrentUser('id') userId: string => retourne uniquement l'id (UUID)
*/
export const CurrentUser = createParamDecorator(
(data: keyof User | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user: User | undefined = request.user;
if (!user) {
return undefined;
}
// Si une clé spécifique est demandée, on retourne uniquement cette propriété
return data ? user[data] : user;
},
);
Le décorateur est volontairement minimaliste : il ne fait que lire request.user, qui a été préalablement renseigné par JwtAuthGuard :
// Extrait de JwtAuthGuard
const user = await this.authService.validateToken(token);
request.user = user;
return true;Point d'attention : ce décorateur ne fait aucune vérification d'authentification. La sécurité reste la responsabilité du guard. Si vous oubliez d'appliquer JwtAuthGuard, request.user sera undefined.
Étape 2 : Améliorer le typage TypeScript
La signature actuelle de data est keyof User | undefined, ce qui offre déjà l'autocomplétion sur les propriétés de User. Cependant, le type de retour reste implicitement any côté consommateur. Pour un typage encore plus strict, on peut utiliser des surcharges :
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '../../database/entities/user.entity';
const extractUser = (
data: keyof User | undefined,
ctx: ExecutionContext,
): User | User[keyof User] | undefined => {
const request = ctx.switchToHttp().getRequest();
const user: User | undefined = request.user;
if (!user) {
return undefined;
}
return data ? user[data] : user;
};
export const CurrentUser = createParamDecorator(extractUser);
En pratique, c'est l'annotation côté contrôleur (user: User ou userId: string) qui guide le typage final. Le décorateur ne peut pas inférer dynamiquement le type retourné en fonction de l'argument passé — c'est une limitation connue de TypeScript avec les décorateurs.
Étape 3 : Utilisation dans les contrôleurs
Reprenons notre AuthController. La version refactorée devient bien plus lisible :
import { Controller, Get, Post, Body } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { ApiResponseDto } from '../../common/dto/api-response.dto';
import { User } from '../../database/entities/user.entity';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Get('me')
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Get current user' })
@ApiResponse({ status: 200, description: 'User information' })
async getProfile(@CurrentUser() user: User): Promise<ApiResponseDto<User>> {
return ApiResponseDto.success(user, 'User retrieved successfully');
}
@Post('refresh')
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Refresh JWT token' })
async refresh(@CurrentUser() user: User): Promise<ApiResponseDto<any>> {
const result = await this.authService.refreshToken(user);
return ApiResponseDto.success(result, 'Token refreshed successfully');
}
}
Variante : extraire un champ spécifique
Très souvent, on n'a besoin que de l'id de l'utilisateur — par exemple pour filtrer ses ressources. Le décorateur supporte ce cas nativement :
@Get('my-bookings')
@ApiBearerAuth('JWT-auth')
async getMyBookings(
@CurrentUser('id') userId: string,
): Promise<ApiResponseDto<Booking[]>> {
const bookings = await this.bookingsService.findByUser(userId);
return ApiResponseDto.success(bookings, 'Bookings retrieved');
}
@Get('my-role')
@ApiBearerAuth('JWT-auth')
async getMyRole(@CurrentUser('role') role: UserRole) {
return ApiResponseDto.success({ role }, 'Role retrieved');
}Notez que userId est typé en string car l'entité User hérite de BaseEntity avec un identifiant UUID.
Étape 4 : Combiner avec des DTOs et la validation
Le décorateur @CurrentUser() brille particulièrement quand on le combine avec des DTOs validés. Imaginons un endpoint qui permet à un client de mettre à jour son profil :
import { IsOptional, IsString, MaxLength, IsPhoneNumber } from 'class-validator';
export class UpdateProfileDto {
@IsOptional()
@IsString()
@MaxLength(100)
firstName?: string;
@IsOptional()
@IsString()
@MaxLength(100)
lastName?: string;
@IsOptional()
@IsPhoneNumber('FR')
phone?: string;
}Et le contrôleur correspondant :
@Patch('me')
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Update current user profile' })
async updateProfile(
@CurrentUser('id') userId: string,
@Body() dto: UpdateProfileDto,
): Promise<ApiResponseDto<User>> {
const updated = await this.usersService.update(userId, dto);
return ApiResponseDto.success(updated, 'Profile updated successfully');
}La séparation des responsabilités est nette : le DTO valide les données entrantes, le décorateur fournit l'identité de l'appelant, et le service applique la logique métier.
Étape 5 : Sécurité et bonnes pratiques
Voici quelques recommandations pour utiliser ce décorateur sereinement :
- Toujours protéger les routes :
@CurrentUser()ne remplace pas le guard. SiJwtAuthGuardest appliqué globalement (viaAPP_GUARD), assurez-vous que les routes publiques sont marquées avec@Public(). - Ne jamais faire confiance au client : pour modifier ou supprimer une ressource, comparez toujours
@CurrentUser('id')avec l'userIdpropriétaire de la ressource côté service. - Combiner avec
@Roles(): pour les opérations privilégiées, ajoutezRolesGuardet le décorateur@Roles(UserRole.ADMIN). - Attention aux propriétés sensibles : dans notre entité,
passwordest marqué{ select: false }, donc il n'est pas chargé par défaut. Vérifiez tout de même que votrevalidateTokenne renvoie pas de données sensibles dansrequest.user.
Étape 6 : Tester le décorateur
Tester un createParamDecorator directement n'est pas trivial car il est compilé par Nest. La bonne pratique consiste à extraire la fonction d'extraction et à la tester unitairement :
// current-user.decorator.spec.ts
import { ExecutionContext } from '@nestjs/common';
// La fonction extraite est exportée séparément pour les tests
import { extractUser } from './current-user.decorator';
describe('CurrentUser decorator', () => {
const buildContext = (user: any): ExecutionContext => ({
switchToHttp: () => ({ getRequest: () => ({ user }) }),
} as any);
it('returns the full user when no key is provided', () => {
const user = { id: 'uuid-1', email: 'a@b.com' };
expect(extractUser(undefined, buildContext(user))).toEqual(user);
});
it('returns the requested property', () => {
const user = { id: 'uuid-1', email: 'a@b.com' };
expect(extractUser('email', buildContext(user))).toBe('a@b.com');
});
it('returns undefined when no user is attached', () => {
expect(extractUser(undefined, buildContext(undefined))).toBeUndefined();
});
});
Conclusion
Avec une vingtaine de lignes de code, nous avons créé un décorateur @CurrentUser() qui rend les contrôleurs NestJS beaucoup plus lisibles, mieux typés et plus testables. Ce pattern est totalement réutilisable d'un projet à l'autre : il suffit d'adapter le type User à votre entité.
Pour aller plus loin :
- Créer un décorateur
@CurrentUserId()dédié pour gagner encore en concision. - Adapter le décorateur au contexte GraphQL via
GqlExecutionContext.create(ctx).getContext().req. - Combiner ce pattern avec d'autres décorateurs personnalisés (
@ClientIp(),@UserAgent()) pour enrichir vos logs et audits. - Consulter la documentation officielle sur les décorateurs personnalisés.
En adoptant ces petits décorateurs utilitaires, vous éliminez progressivement le couplage entre vos contrôleurs et l'objet request brut, ce qui rend votre architecture plus alignée avec les principes SOLID — un bénéfice durable sur la maintenabilité de votre API.

Commentaires