API REST complète avec authentification JWT, gestion des utilisateurs et système de rôles, construite avec NestJS, MongoDB (Mongoose) et TypeScript.
- Fonctionnalités
- Technologies utilisées
- Architecture
- Installation
- Configuration
- Utilisation
- Routes API
- Authentification et autorisation
- Gestion des rôles
- Modèle de données
- Sécurité
- Concepts clés NestJS
- ✅ Authentification JWT (Access Token + Refresh Token)
- ✅ Inscription et connexion des utilisateurs
- ✅ Gestion des utilisateurs (CRUD complet)
- ✅ Système de rôles (User, Admin)
- ✅ Protection des routes par authentification
- ✅ Autorisation basée sur les rôles (RBAC)
- ✅ Protection ownership (un utilisateur peut modifier ses propres données)
- ✅ Validation des données avec class-validator
- ✅ Hashage des mots de passe avec bcrypt
- ✅ Refresh token pour renouveler l'accès
| Technologie | Version | Usage |
|---|---|---|
| NestJS | 10.x | Framework backend TypeScript |
| TypeScript | 5.x | Langage de développement |
| MongoDB | 7.x | Base de données NoSQL |
| Mongoose | 8.x | ODM pour MongoDB |
| Passport | 0.7.x | Middleware d'authentification |
| JWT | 10.x | Tokens d'authentification |
| bcrypt | 5.x | Hashage des mots de passe |
| class-validator | 0.14.x | Validation des DTOs |
| class-transformer | 0.5.x | Transformation des données |
src/
├── auth/ # Module d'authentification
│ ├── decorators/ # Decorators personnalisés
│ │ ├── current-user.decorator.ts # @CurrentUser()
│ │ ├── public.decorator.ts # @Public()
│ │ └── roles.decorator.ts # @Roles()
│ ├── dto/ # Data Transfer Objects
│ │ ├── login.dto.ts # DTO pour la connexion
│ │ ├── register.dto.ts # DTO pour l'inscription
│ │ └── refresh-token.dto.ts # DTO pour le refresh
│ ├── guards/ # Guards de protection
│ │ ├── jwt-auth.guard.ts # Vérifie le JWT
│ │ ├── jwt-refresh-auth.guard.ts # Vérifie le refresh token
│ │ ├── roles.guard.ts # Vérifie les rôles
│ │ └── owner-or-admin.guard.ts # Vérifie ownership ou admin
│ ├── strategies/ # Stratégies Passport
│ │ ├── jwt.strategy.ts # Stratégie JWT
│ │ └── jwt-refresh.strategy.ts # Stratégie refresh token
│ ├── auth.controller.ts # Contrôleur (routes auth)
│ ├── auth.service.ts # Service (logique métier)
│ └── auth.module.ts # Module NestJS
│
├── users/ # Module utilisateurs
│ ├── dto/ # Data Transfer Objects
│ │ ├── create-user.dto.ts # DTO création user
│ │ ├── update-user.dto.ts # DTO mise à jour user
│ │ └── update-user-by-admin.dto.ts # DTO admin
│ ├── schemas/ # Schémas Mongoose
│ │ └── user.schema.ts # Schéma User
│ ├── users.controller.ts # Contrôleur (routes users)
│ ├── users.service.ts # Service (logique métier)
│ └── users.module.ts # Module NestJS
│
├── app.module.ts # Module racine
└── main.ts # Point d'entrée de l'application
- Node.js >= 22.x
- npm ou yarn
- MongoDB (local ou Atlas)
npm installCrée un fichier .env (ou .env.local) à la racine :
# MongoDB
MONGODB_URI=mongodb://localhost:27017/user_crud
# Ou pour MongoDB Atlas :
# MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/user_crud
# JWT
JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
JWT_EXPIRATION=1h
JWT_REFRESH_SECRET=your_super_secret_refresh_key
JWT_REFRESH_EXPIRATION=7d
# Application
PORT=3000
FRONTEND_URL=http://localhost:8080# Linux/WSL
sudo systemctl start mongod
# macOS
brew services start mongodb-community# Mode développement (hot reload)
npm run start:dev
# Mode production
npm run build
npm run start:prodL'API sera accessible sur http://localhost:3000
| Variable | Description | Exemple |
|---|---|---|
MONGODB_URI |
URI de connexion MongoDB | mongodb://localhost:27017/user_crud |
JWT_SECRET |
Secret pour signer les access tokens | my_secret_key |
JWT_EXPIRATION |
Durée de vie des access tokens | 1h, 15m, 7d |
JWT_REFRESH_SECRET |
Secret pour signer les refresh tokens | my_refresh_secret |
JWT_REFRESH_EXPIRATION |
Durée de vie des refresh tokens | 7d, 30d |
PORT |
Port de l'application | 3000 |
FRONTEND_URL |
URL du frontend (pour CORS) | http://localhost:8080 |
- Crée un compte sur MongoDB Atlas
- Crée un cluster gratuit (M0)
- Configure Database Access (crée un utilisateur)
- Configure Network Access
- Récupère la connection string
- Remplace dans
.env:
MONGODB_URI=mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/user_crud?retryWrites=true&w=majority# Connecte-toi à MongoDB
mongosh
# Utilise la base de données
use user_crud
# Crée un admin
db.users.insertOne({
email: "admin@example.com",
password: "$2b$10$...", // Hash de "AdminPass123" avec bcrypt
firstName: "Super",
lastName: "Admin",
roles: ["admin", "user"],
createdAt: new Date(),
updatedAt: new Date()
})- Inscris-toi normalement via
/auth/register - Modifie le rôle directement en DB :
mongosh
use user_crud
db.users.updateOne(
{ email: "ton-email@example.com" },
{ $set: { roles: ["admin", "user"] } }
)POST /auth/register
Content-Type: application/json
{
"email": "john@example.com",
"password": "password123",
"firstName": "John",
"lastName": "Doe"
}Réponse (201) :
{
"user": {
"id": "65a1b2c3d4e5f6g7h8i9j0k1",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"roles": ["user"]
},
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}POST /auth/login
Content-Type: application/json
{
"email": "john@example.com",
"password": "password123"
}Réponse (200) : Identique à /auth/register
POST /auth/refresh
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Réponse (200) :
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Toutes les routes suivantes nécessitent un access token dans le header :
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...GET /auth/meRéponse (200) :
{
"user": {
"id": "65a1b2c3d4e5f6g7h8i9j0k1",
"email": "john@example.com",
"roles": ["user"]
}
}POST /auth/logoutRéponse (200) :
{
"message": "Déconnexion réussie"
}GET /usersRéponse (200) :
[
{
"id": "65a1b2c3d4e5f6g7h8i9j0k1",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"roles": ["user"],
"createdAt": "2026-01-30T10:00:00.000Z",
"updatedAt": "2026-01-30T10:00:00.000Z"
}
]GET /users/meRéponse (200) : Détails de l'utilisateur connecté
GET /users/:idRéponse (200) : Détails de l'utilisateur
PATCH /users/:id
Content-Type: application/json
{
"firstName": "Jane",
"lastName": "Smith"
}Réponse (200) : Utilisateur modifié
Restrictions :
- Un utilisateur peut modifier ses propres données uniquement
- Un admin peut modifier n'importe quel utilisateur
- Un utilisateur normal ne peut pas modifier ses rôles
DELETE /users/:idRéponse (204) : Pas de contenu
L'API utilise 2 types de tokens JWT :
- Utilisé pour toutes les requêtes API
- Envoyé dans le header
Authorization: Bearer <token> - Contient :
{ sub: userId, email, roles } - Expire rapidement pour la sécurité
- Permet de renouveler l'access token sans se reconnecter
- Stocké en base de données (hashé avec bcrypt)
- Vérifié en DB à chaque utilisation
- Révoqué lors de la déconnexion
1. User se connecte → Reçoit accessToken + refreshToken
2. User fait des requêtes avec accessToken
3. AccessToken expire (1h) → Erreur 401
4. Client envoie refreshToken à /auth/refresh
5. Serveur génère nouveaux tokens
6. User peut continuer à utiliser l'API
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2NWExYjJjMyIsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInJvbGVzIjpbInVzZXIiXSwiaWF0IjoxNzA2NjIyMDAwLCJleHAiOjE3MDY2MjU2MDB9.signature
└────────── Header ──────────┘ └──────────────────── Payload (données) ────────────────────────┘ └─ Signature ─┘
Décodage du payload :
{
"sub": "65a1b2c3...", // User ID
"email": "john@example.com",
"roles": ["user"],
"iat": 1706622000, // Issued At
"exp": 1706625600 // Expiration
}| Rôle | Description | Permissions |
|---|---|---|
user |
Utilisateur standard | Accès à son profil, modification de ses données |
admin |
Administrateur | Accès complet à tous les utilisateurs, gestion des rôles |
Toutes les routes (sauf /auth/register et /auth/login) nécessitent un JWT valide.
Certaines routes sont réservées aux admins :
@Roles('admin')
@Get()
findAll() { ... }Un utilisateur peut modifier uniquement ses propres données :
@UseGuards(OwnerOrAdminGuard)
@Patch(':id')
update(@Param('id') id: string) { ... }Le guard vérifie :
- Si
user.id === :id→ ✅ Autorisé (c'est son profil) - OU si
user.roles.includes('admin')→ ✅ Autorisé (c'est un admin) - Sinon → ❌ 403 Forbidden
| Route | Authentification | Autorisation |
|---|---|---|
POST /auth/register |
❌ Publique | Tous |
POST /auth/login |
❌ Publique | Tous |
POST /auth/refresh |
✅ Refresh token | Tous authentifiés |
POST /auth/logout |
✅ JWT | Tous authentifiés |
GET /auth/me |
✅ JWT | Tous authentifiés |
GET /users |
✅ JWT | 👮 Admin seulement |
GET /users/me |
✅ JWT | Tous authentifiés |
GET /users/:id |
✅ JWT | 👮 Admin seulement |
PATCH /users/:id |
✅ JWT | 👤 Owner OU 👮 Admin |
DELETE /users/:id |
✅ JWT | 👮 Admin seulement |
{
_id: ObjectId, // ID MongoDB (auto-généré)
email: String, // Email unique, lowercase, trim
password: String, // Hash bcrypt (jamais en clair)
firstName?: String, // Prénom (optionnel)
lastName?: String, // Nom (optionnel)
roles: String[], // Tableau de rôles ['user', 'admin']
refreshToken?: String, // Refresh token hashé (optionnel)
createdAt: Date, // Date de création (auto)
updatedAt: Date // Date de mise à jour (auto)
}Lors de la sérialisation (réponse API), le schéma est transformé :
// En base de données
{
_id: ObjectId("65a1b2c3d4e5f6g7h8i9j0k1"),
email: "john@example.com",
password: "$2b$10$abcd...",
refreshToken: "$2b$10$wxyz...",
roles: ["user"],
createdAt: ISODate("2026-01-30T10:00:00Z"),
updatedAt: ISODate("2026-01-30T10:00:00Z")
}
// Réponse API (transformé)
{
"id": "65a1b2c3d4e5f6g7h8i9j0k1", // _id → id
"email": "john@example.com",
"roles": ["user"],
"createdAt": "2026-01-30T10:00:00.000Z",
"updatedAt": "2026-01-30T10:00:00.000Z"
// password et refreshToken exclus automatiquement
}Pour optimiser les performances :
// Index sur email (unique + recherche rapide)
UserSchema.index({ email: 1 });- Utilisation de bcrypt avec un salt de 10 rounds
- Les mots de passe ne sont jamais stockés en clair
- Vérification sécurisée avec
bcrypt.compare()
const hashedPassword = await bcrypt.hash(password, 10);
const isValid = await bcrypt.compare(plainPassword, hashedPassword);- Access token : expire en 1h (limite l'exposition)
- Refresh token : stocké hashé en DB, révocable
- Signature avec des secrets forts (JWT_SECRET)
- Validation automatique avec class-validator
- Whitelist activée (propriétés inconnues rejetées)
- Transformation automatique des types
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Retire les props non définies
forbidNonWhitelisted: true, // Erreur si props inconnues
transform: true // Transforme les types auto
}));- Le
passwordn'est jamais retourné dans les réponses API - Le
refreshTokenest également exclu - Transformation automatique via
@Exclude()ettoJSON
- Seules les origines autorisées peuvent accéder à l'API
- Configuration via
FRONTEND_URL
app.enableCors({
origin: process.env.FRONTEND_URL,
credentials: true
});✅ En production :
- Change les secrets JWT (
JWT_SECRET,JWT_REFRESH_SECRET) - Utilise HTTPS uniquement
- Active
synchronize: falsedans TypeORM/Mongoose - Configure des variables d'environnement sécurisées
- Mets en place un rate limiting (limiteur de requêtes)
- Active les logs d'audit
- Utilise un reverse proxy (Nginx, Traefik)
❌ Ne jamais :
- Exposer les secrets dans le code source
- Stocker des mots de passe en clair
- Retourner des erreurs détaillées en production
- Accepter
synchronize: trueen prod (risque de perte de données)
Les modules organisent l'application en blocs fonctionnels :
@Module({
imports: [MongooseModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService] // Rend le service disponible ailleurs
})
export class UsersModule {}Gèrent les routes HTTP et délèguent la logique aux services :
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
}Contiennent la logique métier :
@Injectable()
export class UsersService {
constructor(@InjectModel(User.name) private userModel: Model<User>) {}
async findAll(): Promise<User[]> {
return this.userModel.find().exec();
}
}Protègent les routes (authentification, autorisation) :
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// Utilisation
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile() { ... }Ajoutent des métadonnées ou extraient des données :
// Définition
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
}
);
// Utilisation
@Get('me')
getMe(@CurrentUser() user) {
return user;
}Définissent la structure et valident les données :
export class CreateUserDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
}Gèrent les mécanismes d'authentification :
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
async validate(payload: JwtPayload) {
// Charge et retourne l'utilisateur
return { id: payload.sub, email: payload.email, roles: payload.roles };
}
}NestJS injecte automatiquement les dépendances :
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService, // ← Injecté auto
private jwtService: JwtService // ← Injecté auto
) {}
}Si tu viens de Symfony, voici les équivalences :
| Symfony | NestJS |
|---|---|
| Bundle | Module |
| Controller | Controller |
| Service | Service (Provider) |
| Entity | Schema (Mongoose) |
| Repository | Model (Mongoose) |
| FormType | DTO |
| Validator | class-validator |
| Security/Authenticator | Strategy (Passport) |
| #[IsGranted()] | @UseGuards(RolesGuard) |
| $this->getUser() | @CurrentUser() |
| Security/Voter | Custom Guard |
| EventDispatcher | EventEmitter |
| DependencyInjection | Dependency Injection |
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123",
"firstName": "Test",
"lastName": "User"
}'curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123"
}'Sauvegarde le token retourné dans une variable :
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."curl -X GET http://localhost:3000/auth/me \
-H "Authorization: Bearer $TOKEN"curl -X PATCH http://localhost:3000/users/<USER_ID> \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Nouveau Prénom"
}'REFRESH_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -X POST http://localhost:3000/auth/refresh \
-H "Content-Type: application/json" \
-d "{\"refreshToken\": \"$REFRESH_TOKEN\"}"curl -X POST http://localhost:3000/auth/logout \
-H "Authorization: Bearer $TOKEN"# Développement
npm run start:dev # Lance en mode watch (hot reload)
npm run start:debug # Lance en mode debug
# Production
npm run build # Compile TypeScript → JavaScript
npm run start:prod # Lance la version compilée
# Génération de code
nest g module nom # Crée un module
nest g controller nom # Crée un contrôleur
nest g service nom # Crée un service
nest g resource nom # Crée module + controller + service + DTOs
# MongoDB
mongosh # Ouvre le shell MongoDB
use user_crud # Sélectionne la DB
db.users.find() # Liste tous les users
db.users.countDocuments() # Compte les usersCause : MongoDB n'est pas lancé
Solution :
sudo systemctl start mongodCause : Token invalide ou secret JWT incorrect
Solution :
- Vérifie que
JWT_SECRETdans.envcorrespond - Assure-toi d'envoyer le bon token
- Reconnecte-toi pour obtenir un nouveau token
Cause : Validation DTO échouée
Solution :
- Vérifie le format de l'email
- Assure-toi d'envoyer tous les champs requis
Cause : Permissions insuffisantes
Solution :
- Vérifie que ton user a le rôle requis (
admin, etc.) - Pour les routes ownership, vérifie que tu modifies tes propres données
- Documentation NestJS
- Documentation Mongoose
- Documentation Passport JWT
- class-validator
- MongoDB Atlas
Ce projet est à usage éducatif.
Développé dans le cadre de l'apprentissage de NestJS, TypeScript et MongoDB.
Prochaines étapes recommandées :
- Protection contre l'escalade de privilèges
- Ajouter la pagination et le filtrage
- Implémenter l'upload de fichiers (photo de profil)
- Ajouter la vérification par email
- Créer un système de reset de mot de passe
- Ajouter des tests
- Générer la documentation Swagger/OpenAPI
- Dockeriser l'application
- Mettre en place un CI/CD