Construire des API REST production avec NestJS, TypeORM et PostgreSQL

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

NestJS est le framework Node.js le plus populaire pour les entreprises — et ce n'est pas un hasard. Construit avec TypeScript, inspiré de l'architecture Angular et propulsé par Express (ou Fastify) en coulisses, NestJS apporte structure, testabilité et scalabilité au développement backend. Combiné avec TypeORM et PostgreSQL, il forme une stack prête pour la production utilisée par des entreprises comme Adidas, Roche et Autodesk.

Ce que vous allez construire

Une API de gestion des utilisateurs complète avec :

  • Architecture modulaire NestJS avec contrôleurs, services et repositories
  • Base de données PostgreSQL avec entités TypeORM et migrations
  • Authentification JWT (inscription, connexion, routes protégées)
  • Validation des requêtes avec class-validator et DTOs
  • Gestion structurée des erreurs avec filtres d'exceptions
  • Pagination, filtrage et tri
  • Configuration basée sur l'environnement avec @nestjs/config
  • Tests unitaires et end-to-end

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé
  • PostgreSQL 15+ en cours d'exécution localement ou via Docker
  • Gestionnaire de paquets npm ou yarn
  • Compréhension de base de TypeScript et des concepts REST
  • Un éditeur de code (VS Code recommandé)

Nouveau avec TypeScript ? Vous devriez être à l'aise avec les classes, décorateurs, interfaces et génériques. Le TypeScript Handbook officiel est un excellent point de départ.


Pourquoi NestJS ?

Avant de plonger, comprenons ce qui distingue NestJS :

CaractéristiqueNestJSExpressFastifyHono
ArchitectureStructurée (modules)MinimaleMinimaleMinimale
TypeScriptNatifAdd-onAdd-onNatif
Conteneur DIIntégréAucunAucunAucun
CLIScaffolding completAucunAucunAucun
TestsUtilitaires intégrésManuelManuelManuel
Courbe d'apprentissageModéréeFaibleFaibleFaible

NestJS échange la flexibilité d'Express contre une approche structurée et opinionnée qui porte ses fruits à mesure que votre projet grandit. Si vous avez travaillé avec Angular ou Spring Boot, les patterns vous seront familiers.


Étape 1 : Initialisation du projet

Installez le CLI NestJS globalement et créez un nouveau projet :

npm i -g @nestjs/cli
nest new user-api

Choisissez npm quand on vous le demande. Puis naviguez dans le projet :

cd user-api

Installez les dépendances requises :

npm install @nestjs/typeorm typeorm pg
npm install @nestjs/config
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install class-validator class-transformer
npm install bcrypt
npm install -D @types/passport-jwt @types/bcrypt

La structure de votre projet ressemble à ceci :

user-api/
├── src/
│   ├── app.module.ts       # Module racine
│   ├── app.controller.ts   # Contrôleur racine
│   ├── app.service.ts      # Service racine
│   └── main.ts             # Point d'entrée
├── test/
│   └── app.e2e-spec.ts     # Tests E2E
├── nest-cli.json
├── tsconfig.json
└── package.json

Étape 2 : Configuration de la base de données

Créez un fichier .env à la racine du projet :

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=user_api
JWT_SECRET=your-super-secret-key-change-in-production
JWT_EXPIRES_IN=1h

Mettez à jour app.module.ts pour configurer TypeORM et les variables d'environnement :

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
 
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get('DATABASE_HOST'),
        port: config.get<number>('DATABASE_PORT'),
        username: config.get('DATABASE_USER'),
        password: config.get('DATABASE_PASSWORD'),
        database: config.get('DATABASE_NAME'),
        autoLoadEntities: true,
        synchronize: process.env.NODE_ENV !== 'production',
      }),
    }),
  ],
})
export class AppModule {}

N'utilisez jamais synchronize: true en production. Cette option modifie automatiquement le schéma de votre base de données à chaque démarrage, ce qui peut causer des pertes de données. Utilisez les migrations à la place — nous les configurerons plus tard.


Étape 3 : Créer l'entité User

Générez le module users avec le CLI :

nest g module users
nest g controller users
nest g service users

Créez l'entité User :

// src/users/entities/user.entity.ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm';
 
@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;
 
  @Column({ length: 100 })
  name: string;
 
  @Column({ unique: true })
  email: string;
 
  @Column({ select: false })
  password: string;
 
  @Column({ default: 'user' })
  role: string;
 
  @Column({ default: true })
  isActive: boolean;
 
  @CreateDateColumn()
  createdAt: Date;
 
  @UpdateDateColumn()
  updatedAt: Date;
}

Enregistrez l'entité dans le module users :

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
 
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

Étape 4 : DTOs et validation

Créez les Data Transfer Objects pour valider les requêtes entrantes :

// src/users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
 
export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;
 
  @IsEmail()
  email: string;
 
  @IsString()
  @MinLength(8)
  password: string;
 
  @IsOptional()
  @IsString()
  role?: string;
}
// src/users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
 
export class UpdateUserDto extends PartialType(CreateUserDto) {}

Créez un DTO de requête pour la pagination et le filtrage :

// src/users/dto/query-user.dto.ts
import { IsOptional, IsInt, Min, IsString } from 'class-validator';
import { Type } from 'class-transformer';
 
export class QueryUserDto {
  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  page?: number = 1;
 
  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  limit?: number = 10;
 
  @IsOptional()
  @IsString()
  search?: string;
 
  @IsOptional()
  @IsString()
  role?: string;
}

Activez la validation globalement dans main.ts :

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
 
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );
 
  app.enableCors();
  app.setGlobalPrefix('api/v1');
 
  await app.listen(3000);
  console.log(`Application running on: ${await app.getUrl()}`);
}
bootstrap();

Étape 5 : Service Users (logique métier)

Implémentez le service avec les opérations CRUD, la pagination et le hachage des mots de passe :

// src/users/users.service.ts
import {
  Injectable,
  NotFoundException,
  ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { QueryUserDto } from './dto/query-user.dto';
 
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}
 
  async create(createUserDto: CreateUserDto): Promise<User> {
    const existing = await this.usersRepository.findOne({
      where: { email: createUserDto.email },
    });
 
    if (existing) {
      throw new ConflictException('Email déjà enregistré');
    }
 
    const hashedPassword = await bcrypt.hash(createUserDto.password, 12);
 
    const user = this.usersRepository.create({
      ...createUserDto,
      password: hashedPassword,
    });
 
    const saved = await this.usersRepository.save(user);
    delete saved.password;
    return saved;
  }
 
  async findAll(query: QueryUserDto) {
    const { page, limit, search, role } = query;
    const skip = (page - 1) * limit;
 
    const where: any = {};
    if (role) where.role = role;
    if (search) where.name = Like(`%${search}%`);
 
    const [users, total] = await this.usersRepository.findAndCount({
      where,
      skip,
      take: limit,
      order: { createdAt: 'DESC' },
    });
 
    return {
      data: users,
      meta: {
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
      },
    };
  }
 
  async findOne(id: string): Promise<User> {
    const user = await this.usersRepository.findOne({ where: { id } });
    if (!user) {
      throw new NotFoundException(`Utilisateur avec l'ID "${id}" non trouvé`);
    }
    return user;
  }
 
  async findByEmail(email: string): Promise<User> {
    return this.usersRepository.findOne({
      where: { email },
      select: ['id', 'name', 'email', 'password', 'role', 'isActive'],
    });
  }
 
  async update(id: string, updateUserDto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id);
 
    if (updateUserDto.password) {
      updateUserDto.password = await bcrypt.hash(updateUserDto.password, 12);
    }
 
    Object.assign(user, updateUserDto);
    return this.usersRepository.save(user);
  }
 
  async remove(id: string): Promise<void> {
    const user = await this.findOne(id);
    await this.usersRepository.remove(user);
  }
}

Étape 6 : Contrôleur Users (couche HTTP)

Créez le contrôleur avec les méthodes HTTP et codes de statut appropriés :

// src/users/users.controller.ts
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  Query,
  ParseUUIDPipe,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { QueryUserDto } from './dto/query-user.dto';
 
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
 
  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
 
  @Get()
  findAll(@Query() query: QueryUserDto) {
    return this.usersService.findAll(query);
  }
 
  @Get(':id')
  findOne(@Param('id', ParseUUIDPipe) id: string) {
    return this.usersService.findOne(id);
  }
 
  @Put(':id')
  update(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto);
  }
 
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseUUIDPipe) id: string) {
    return this.usersService.remove(id);
  }
}

Étape 7 : Authentification avec JWT

Générez le module auth :

nest g module auth
nest g controller auth
nest g service auth

Créez la stratégie JWT :

// src/auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../../users/users.service';
 
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private configService: ConfigService,
    private usersService: UsersService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }
 
  async validate(payload: { sub: string; email: string }) {
    const user = await this.usersService.findOne(payload.sub);
    if (!user || !user.isActive) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Implémentez le service d'authentification :

// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
import { CreateUserDto } from '../users/dto/create-user.dto';
 
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}
 
  async register(createUserDto: CreateUserDto) {
    const user = await this.usersService.create(createUserDto);
    const token = this.generateToken(user.id, user.email);
    return { user, access_token: token };
  }
 
  async login(email: string, password: string) {
    const user = await this.usersService.findByEmail(email);
 
    if (!user || !(await bcrypt.compare(password, user.password))) {
      throw new UnauthorizedException('Identifiants invalides');
    }
 
    delete user.password;
    const token = this.generateToken(user.id, user.email);
    return { user, access_token: token };
  }
 
  private generateToken(userId: string, email: string): string {
    return this.jwtService.sign({ sub: userId, email });
  }
}

Créez le contrôleur d'authentification :

// src/auth/auth.controller.ts
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService } from './auth.service';
import { CreateUserDto } from '../users/dto/create-user.dto';
 
class LoginDto {
  email: string;
  password: string;
}
 
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}
 
  @Post('register')
  register(@Body() createUserDto: CreateUserDto) {
    return this.authService.register(createUserDto);
  }
 
  @Post('login')
  @HttpCode(HttpStatus.OK)
  login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto.email, loginDto.password);
  }
}

Assemblez le module auth :

// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UsersModule } from '../users/users.module';
 
@Module({
  imports: [
    UsersModule,
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        secret: config.get('JWT_SECRET'),
        signOptions: { expiresIn: config.get('JWT_EXPIRES_IN', '1h') },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

Étape 8 : Protéger les routes avec des guards

Créez un guard d'authentification JWT :

// src/auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

Créez un guard de rôles pour l'autorisation :

// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
 
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) =>
  Reflect.metadata(ROLES_KEY, roles);
 
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
 
  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(
      ROLES_KEY,
      [context.getHandler(), context.getClass()],
    );
 
    if (!requiredRoles) return true;
 
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.role);
  }
}

Protégez maintenant le contrôleur users :

// src/users/users.controller.ts (mis à jour)
import {
  Controller,
  Get,
  Put,
  Delete,
  Body,
  Param,
  Query,
  ParseUUIDPipe,
  HttpCode,
  HttpStatus,
  UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard, Roles } from '../auth/guards/roles.guard';
import { UsersService } from './users.service';
import { UpdateUserDto } from './dto/update-user.dto';
import { QueryUserDto } from './dto/query-user.dto';
 
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
 
  @Get()
  findAll(@Query() query: QueryUserDto) {
    return this.usersService.findAll(query);
  }
 
  @Get(':id')
  findOne(@Param('id', ParseUUIDPipe) id: string) {
    return this.usersService.findOne(id);
  }
 
  @Put(':id')
  update(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto);
  }
 
  @Delete(':id')
  @Roles('admin')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseUUIDPipe) id: string) {
    return this.usersService.remove(id);
  }
}

Étape 9 : Filtres d'exceptions et gestion des erreurs

Créez un filtre d'exceptions global pour des réponses d'erreur cohérentes :

// src/common/filters/http-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
 
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
 
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;
 
    const message =
      exception instanceof HttpException
        ? exception.getResponse()
        : 'Erreur interne du serveur';
 
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      ...(typeof message === 'string' ? { message } : message),
    });
  }
}

Enregistrez-le globalement dans main.ts :

// Ajoutez à la fonction bootstrap de main.ts
import { AllExceptionsFilter } from './common/filters/http-exception.filter';
 
app.useGlobalFilters(new AllExceptionsFilter());

Étape 10 : Migrations de base de données

Configurez le CLI TypeORM pour les migrations. Créez typeorm.config.ts à la racine du projet :

// typeorm.config.ts
import { DataSource } from 'typeorm';
import * as dotenv from 'dotenv';
 
dotenv.config();
 
export default new DataSource({
  type: 'postgres',
  host: process.env.DATABASE_HOST,
  port: parseInt(process.env.DATABASE_PORT, 10),
  username: process.env.DATABASE_USER,
  password: process.env.DATABASE_PASSWORD,
  database: process.env.DATABASE_NAME,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/migrations/*.ts'],
});

Ajoutez les scripts de migration dans package.json :

{
  "scripts": {
    "migration:generate": "typeorm migration:generate -d typeorm.config.ts",
    "migration:run": "typeorm migration:run -d typeorm.config.ts",
    "migration:revert": "typeorm migration:revert -d typeorm.config.ts"
  }
}

Générez et exécutez votre première migration :

npx ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm migration:generate src/migrations/CreateUsers -d typeorm.config.ts
 
npx ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm migration:run -d typeorm.config.ts

Étape 11 : Tests

NestJS inclut d'excellents utilitaires de test. Écrivez un test unitaire pour le service users :

// src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { NotFoundException } from '@nestjs/common';
 
const mockUser: Partial<User> = {
  id: '550e8400-e29b-41d4-a716-446655440000',
  name: 'John Doe',
  email: 'john@example.com',
  role: 'user',
  isActive: true,
};
 
describe('UsersService', () => {
  let service: UsersService;
  let repository: Repository<User>;
 
  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useValue: {
            findOne: jest.fn(),
            findAndCount: jest.fn(),
            create: jest.fn(),
            save: jest.fn(),
            remove: jest.fn(),
          },
        },
      ],
    }).compile();
 
    service = module.get<UsersService>(UsersService);
    repository = module.get(getRepositoryToken(User));
  });
 
  describe('findOne', () => {
    it('devrait retourner un utilisateur par ID', async () => {
      jest.spyOn(repository, 'findOne').mockResolvedValue(mockUser as User);
 
      const result = await service.findOne(mockUser.id);
      expect(result).toEqual(mockUser);
    });
 
    it('devrait lancer NotFoundException pour un ID invalide', async () => {
      jest.spyOn(repository, 'findOne').mockResolvedValue(null);
 
      await expect(service.findOne('invalid-id')).rejects.toThrow(
        NotFoundException,
      );
    });
  });
 
  describe('findAll', () => {
    it('devrait retourner des utilisateurs paginés', async () => {
      jest
        .spyOn(repository, 'findAndCount')
        .mockResolvedValue([[mockUser as User], 1]);
 
      const result = await service.findAll({ page: 1, limit: 10 });
      expect(result.data).toHaveLength(1);
      expect(result.meta.total).toBe(1);
    });
  });
});

Écrivez un test e2e :

// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
 
describe('Users (e2e)', () => {
  let app: INestApplication;
  let authToken: string;
 
  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
 
    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
    app.setGlobalPrefix('api/v1');
    await app.init();
 
    // Inscription et obtention du token
    const res = await request(app.getHttpServer())
      .post('/api/v1/auth/register')
      .send({
        name: 'Test User',
        email: 'test@example.com',
        password: 'password123',
      });
 
    authToken = res.body.access_token;
  });
 
  it('/api/v1/users (GET) - devrait exiger une authentification', () => {
    return request(app.getHttpServer())
      .get('/api/v1/users')
      .expect(401);
  });
 
  it('/api/v1/users (GET) - devrait retourner les utilisateurs avec auth', () => {
    return request(app.getHttpServer())
      .get('/api/v1/users')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200)
      .expect((res) => {
        expect(res.body.data).toBeDefined();
        expect(res.body.meta).toBeDefined();
      });
  });
 
  afterAll(async () => {
    await app.close();
  });
});

Lancez vos tests :

# Tests unitaires
npm run test
 
# Tests E2E
npm run test:e2e
 
# Couverture de tests
npm run test:cov

Étape 12 : Structure finale du projet

Votre projet terminé devrait ressembler à ceci :

user-api/
├── src/
│   ├── auth/
│   │   ├── guards/
│   │   │   ├── jwt-auth.guard.ts
│   │   │   └── roles.guard.ts
│   │   ├── strategies/
│   │   │   └── jwt.strategy.ts
│   │   ├── auth.controller.ts
│   │   ├── auth.module.ts
│   │   └── auth.service.ts
│   ├── common/
│   │   └── filters/
│   │       └── http-exception.filter.ts
│   ├── migrations/
│   │   └── ...CreateUsers.ts
│   ├── users/
│   │   ├── dto/
│   │   │   ├── create-user.dto.ts
│   │   │   ├── update-user.dto.ts
│   │   │   └── query-user.dto.ts
│   │   ├── entities/
│   │   │   └── user.entity.ts
│   │   ├── users.controller.ts
│   │   ├── users.module.ts
│   │   ├── users.service.ts
│   │   └── users.service.spec.ts
│   ├── app.module.ts
│   └── main.ts
├── test/
│   └── users.e2e-spec.ts
├── .env
├── typeorm.config.ts
└── package.json

Tester votre API

Démarrez l'application :

npm run start:dev

Testez les endpoints avec curl :

# Inscrire un nouvel utilisateur
curl -X POST http://localhost:3000/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com", "password": "securepass123"}'
 
# Se connecter
curl -X POST http://localhost:3000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@example.com", "password": "securepass123"}'
 
# Obtenir tous les utilisateurs (avec token)
curl http://localhost:3000/api/v1/users \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"
 
# Obtenir les utilisateurs avec pagination
curl "http://localhost:3000/api/v1/users?page=1&limit=5&search=alice" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

Dépannage

"Cannot find module 'pg'" — Installez le driver PostgreSQL : npm install pg

"relation users does not exist" — Exécutez les migrations ou activez synchronize: true temporairement en développement.

"Unauthorized" sur toutes les routes — Vérifiez que JWT_SECRET correspond entre .env et la génération du token. Vérifiez que le token n'a pas expiré.

La validation ne fonctionne pas — Assurez-vous que ValidationPipe est enregistré globalement dans main.ts et que les décorateurs class-validator sont sur les propriétés de vos DTOs.

Erreurs de dépendance circulaire — Utilisez forwardRef() quand deux modules dépendent l'un de l'autre : @Inject(forwardRef(() => UsersService)).


Prochaines étapes

Maintenant que vous avez une base solide, envisagez d'étendre votre API avec :

  • Documentation Swagger — Ajoutez @nestjs/swagger pour une documentation API auto-générée
  • Limitation de débit — Utilisez @nestjs/throttler pour vous protéger contre les abus
  • Mise en cache — Implémentez un cache Redis avec @nestjs/cache-manager
  • Upload de fichiers — Ajoutez @nestjs/platform-express avec Multer
  • WebSockets — Ajoutez des fonctionnalités temps réel avec @nestjs/websockets
  • GraphQL — Remplacez REST par @nestjs/graphql et Apollo Server
  • Microservices — Divisez en microservices avec @nestjs/microservices

Conclusion

Vous avez construit une API REST prête pour la production avec NestJS, TypeORM et PostgreSQL. Vous avez appris comment l'architecture modulaire de NestJS favorise une organisation propre du code, comment TypeORM simplifie les opérations de base de données, et comment implémenter une authentification JWT avec un contrôle d'accès basé sur les rôles.

L'approche structurée de NestJS peut sembler lourde pour les petits projets, mais elle brille à mesure que votre application grandit — le système d'injection de dépendances, les utilitaires de test intégrés et le design modulaire facilitent l'ajout de fonctionnalités sans accumuler de dette technique. C'est pourquoi NestJS reste le choix privilégié pour les applications Node.js en entreprise.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Construire des Agents IA Stateful avec LangGraph.js et TypeScript.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire des API REST avec Go et Fiber : Guide pratique pour débutants

Apprenez à construire des API REST rapides et prêtes pour la production avec Go et le framework Fiber. Ce guide pas à pas couvre la configuration du projet, le routage, le traitement JSON, la connexion à la base de données avec GORM, les middlewares, la gestion des erreurs et les tests — de zéro à une API fonctionnelle.

30 min read·