π Projet Final
ββββ Expert
~20h
NestJS 10 + TS 5
π Social API β Backend Complet
Construire une API sociale production-ready qui combine tous les concepts de la formation : authentification JWT, CRUD avec TypeORM, WebSockets temps rΓ©el, queues BullMQ, tests complets et dΓ©ploiement Docker/CI-CD.
NestJS 10
TypeScript 5
TypeORM 0.3
PostgreSQL
Socket.io
BullMQ
Redis
Jest 85%+
Docker
GitHub Actions
ποΈ Architecture de l'Application
social-api/
βββ src/
β βββ app.module.ts
β βββ main.ts
β βββ auth/
β β βββ auth.module.ts
β β βββ auth.controller.ts β /auth/register, login, refresh, logout, me
β β βββ auth.service.ts
β β βββ strategies/
β β β βββ local.strategy.ts
β β β βββ jwt.strategy.ts
β β β βββ jwt-refresh.strategy.ts
β β βββ guards/
β β βββ jwt-auth.guard.ts β global (+ @Public())
β β βββ roles.guard.ts
β β βββ resource-owner.guard.ts
β βββ users/
β β βββ users.module.ts
β β βββ users.controller.ts β GET /users/:id, PUT /users/:id, follow/unfollow
β β βββ users.service.ts
β β βββ entities/user.entity.ts
β β βββ dto/
β β βββ create-user.dto.ts
β β βββ update-user.dto.ts
β βββ posts/
β β βββ posts.module.ts
β β βββ posts.controller.ts β CRUD + feed + timeline
β β βββ posts.service.ts
β β βββ entities/post.entity.ts
β β βββ dto/
β β βββ create-post.dto.ts
β β βββ update-post.dto.ts
β βββ likes/
β β βββ likes.module.ts
β β βββ likes.controller.ts β POST /posts/:id/like, DELETE /posts/:id/like
β β βββ likes.service.ts
β β βββ entities/like.entity.ts
β βββ comments/
β β βββ comments.module.ts
β β βββ comments.controller.ts β CRUD + nested replies
β β βββ comments.service.ts
β β βββ entities/comment.entity.ts
β βββ notifications/
β β βββ notifications.module.ts
β β βββ notifications.gateway.ts β WebSocket temps rΓ©el
β β βββ notifications.service.ts
β β βββ entities/notification.entity.ts
β βββ mail/
β β βββ mail.module.ts β dynamic module
β β βββ mail.service.ts
β β βββ processors/mail.processor.ts β BullMQ
β βββ common/
β β βββ decorators/
β β β βββ current-user.decorator.ts
β β β βββ public.decorator.ts
β β β βββ roles.decorator.ts
β β βββ interceptors/
β β β βββ transform.interceptor.ts
β β β βββ logging.interceptor.ts
β β β βββ timeout.interceptor.ts
β β βββ filters/
β β β βββ all-exceptions.filter.ts
β β βββ middleware/
β β β βββ logger.middleware.ts
β β βββ pipes/
β β βββ parse-positive-int.pipe.ts
β βββ health/
β βββ health.controller.ts
βββ test/
β βββ auth.e2e-spec.ts
β βββ posts.e2e-spec.ts
βββ Dockerfile
βββ docker-compose.yml
βββ docker-compose.prod.yml
βββ .env.example
βββ .github/
βββ workflows/
βββ ci.yml
βββ deploy.yml
ποΈ EntitΓ©s TypeORM
// user.entity.ts
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ unique: true }) email: string;
@Column({ unique: true }) username: string;
@Column({ select: false }) passwordHash: string;
@Column({ nullable: true }) bio: string;
@Column({ nullable: true }) avatarUrl: string;
@Column({ type: 'enum', enum: Role, default: Role.USER }) role: Role;
@Column({ nullable: true, select: false }) refreshTokenHash: string;
@OneToMany(() => Post, post => post.author) posts: Post[];
@OneToMany(() => Like, like => like.user) likes: Like[];
@OneToMany(() => Notification, n => n.recipient) notifications: Notification[];
// Self-referential ManyToMany for follows
@ManyToMany(() => User, user => user.following)
@JoinTable({ name: 'user_follows', joinColumn: { name: 'follower_id' }, inverseJoinColumn: { name: 'following_id' } })
followers: User[];
@ManyToMany(() => User, user => user.followers)
following: User[];
@CreateDateColumn() createdAt: Date;
@UpdateDateColumn() updatedAt: Date;
}
// post.entity.ts
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'text' }) content: string;
@Column({ nullable: true }) imageUrl: string;
@Column({ type: 'simple-array', default: '' }) tags: string[];
@ManyToOne(() => User, user => user.posts, { eager: true, onDelete: 'CASCADE' })
author: User;
@OneToMany(() => Like, like => like.post) likes: Like[];
@OneToMany(() => Comment, comment => comment.post) comments: Comment[];
@Column({ default: 0 }) likesCount: number;
@Column({ default: 0 }) commentsCount: number;
@CreateDateColumn() createdAt: Date;
@UpdateDateColumn() updatedAt: Date;
@DeleteDateColumn() deletedAt: Date; // soft delete
}
// like.entity.ts
@Entity('likes')
@Unique(['user', 'post']) // empΓͺche les doublons
export class Like {
@PrimaryGeneratedColumn('uuid') id: string;
@ManyToOne(() => User, user => user.likes, { onDelete: 'CASCADE' }) user: User;
@ManyToOne(() => Post, post => post.likes, { onDelete: 'CASCADE' }) post: Post;
@CreateDateColumn() createdAt: Date;
}
// notification.entity.ts
@Entity('notifications')
export class Notification {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'enum', enum: NotificationType }) type: NotificationType;
@Column({ type: 'jsonb' }) payload: Record<string, any>;
@Column({ default: false }) read: boolean;
@ManyToOne(() => User, user => user.notifications, { onDelete: 'CASCADE' })
recipient: User;
@Column({ nullable: true }) actorId: string;
@CreateDateColumn() createdAt: Date;
}
π‘ Endpoints REST
| MΓ©thode | Route | Description | Auth |
|---|---|---|---|
POST |
/auth/register |
Inscription + email de bienvenue (BullMQ) | Public |
POST |
/auth/login |
Login β accessToken (15m) + refreshToken (7j) | Public |
POST |
/auth/refresh |
Rotation des tokens (dΓ©tecte rΓ©utilisation) | Refresh JWT |
POST |
/auth/logout |
Invalider refreshToken en DB | JWT |
GET |
/auth/me |
Profil courant (serialisΓ© sans hash) | JWT |
GET |
/users/:id |
Profil public + stats (followers, following, posts) | JWT |
PUT |
/users/:id |
Modifier profil (ResourceOwnerGuard) | JWT + Owner |
POST |
/users/:id/follow |
S'abonner + notif WebSocket | JWT |
DELETE |
/users/:id/follow |
Se dΓ©sabonner | JWT |
GET |
/posts |
Feed global paginΓ© (cursor-based) | JWT |
GET |
/posts/timeline |
Timeline des utilisateurs suivis | JWT |
POST |
/posts |
CrΓ©er post + notifier followers via WebSocket | JWT |
PATCH |
/posts/:id |
Modifier post (auteur seulement) | JWT + Owner |
DELETE |
/posts/:id |
Soft delete (auteur ou admin) | JWT + Owner/Admin |
POST |
/posts/:id/like |
Liker + notif WebSocket Γ l'auteur | JWT |
DELETE |
/posts/:id/like |
Retirer le like | JWT |
GET |
/posts/:id/comments |
Commentaires paginΓ©s + replies | JWT |
POST |
/posts/:id/comments |
Commenter + notif Γ l'auteur du post | JWT |
GET |
/notifications |
Notifs de l'utilisateur courant (non lues en premier) | JWT |
GET |
/health |
Status DB + Redis + mΓ©moire + disque | Public |
β‘ WebSocket β Notifications Temps RΓ©el
// notifications.gateway.ts
@WebSocketGateway({ namespace: '/notifications', cors: { origin: '*' } })
export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server: Server;
async handleConnection(client: Socket) {
const user = await this.authService.validateWsToken(client.handshake.auth.token);
if (!user) return client.disconnect();
client.join(`user:${user.id}`); // salle privΓ©e par userId
await this.notificationsService.markSocketConnected(user.id, client.id);
}
// Envoyer une notif en temps rΓ©el
sendToUser(userId: string, notification: NotificationDto) {
this.server.to(`user:${userId}`).emit('notification:new', notification);
}
// Marquer comme lu cΓ΄tΓ© client
@SubscribeMessage('notification:read')
async markRead(@ConnectedSocket() client: Socket, @MessageBody() id: string) {
const user = this.getUser(client);
return this.notificationsService.markAsRead(user.id, id);
}
}
// Types de notifications Γ©mises
// β 'notification:new' { type, message, actorUsername, link, createdAt }
// β 'post:liked' quand un post de l'user reΓ§oit un like
// β 'post:commented' quand un post de l'user reΓ§oit un commentaire
// β 'user:followed' quand un user suit l'utilisateur courant
π¬ Queue BullMQ β Emails
// Types de jobs dans la queue 'mail'
type MailJob =
| { type: 'welcome'; to: string; username: string }
| { type: 'notification'; to: string; subject: string; items: string[] }
| { type: 'digest'; to: string; username: string; unreadCount: number };
// mail.processor.ts
@Processor('mail')
export class MailProcessor {
@Process('welcome')
async sendWelcome(job: Job<WelcomeJobData>) {
await this.mailService.send({ to: job.data.to, subject: 'Bienvenue sur Social API', ... });
}
@Process('notification')
async sendNotification(job: Job<NotifJobData>) {
await this.mailService.send({ to: job.data.to, subject: job.data.subject, ... });
}
}
// Jobs dΓ©clenchΓ©s automatiquement
// register β 'welcome' job (dΓ©lai 0s)
// like/comment/follow β 'notification' job si user offline (dΓ©lai 30s)
// daily digest cron β 'digest' jobs pour users avec notifs non lues
π§ͺ Suite de Tests Attendue
src/
βββ auth/
β βββ auth.service.spec.ts β register, login, refresh, reuse detection
βββ posts/
β βββ posts.service.spec.ts β CRUD, timeline, soft delete
β βββ posts.controller.spec.ts β routes, guards, transformations
βββ likes/
β βββ likes.service.spec.ts β toggle like, doublon @Unique
βββ notifications/
β βββ notifications.gateway.spec.ts β WS connect/disconnect, emit
βββ common/
βββ interceptors/transform.interceptor.spec.ts
βββ filters/all-exceptions.filter.spec.ts
test/
βββ auth.e2e-spec.ts β register β login β refresh β logout
βββ posts.e2e-spec.ts β create β like β comment β delete
85%
Statements
80%
Branches
90%
Functions
85%
Lines
π³ DΓ©ploiement
# docker-compose.yml (dev)
services:
api:
build: .
ports: ["3000:3000"]
env_file: .env
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_started }
postgres:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
interval: 5s
redis:
image: redis:7-alpine
command: redis-server --requirepass $$REDIS_PASSWORD
pgadmin:
image: dpage/pgadmin4:latest
ports: ["5050:80"]
# Pipeline CI/CD (GitHub Actions)
# ββββββββββββ ββββββββββββββββββββ ββββββββββββββββ
# β ci.yml ββββββΆβ build-push.yml ββββββΆβ deploy.yml β
# β lint+testβ β Docker β GHCR β β SSH + pull β
# ββββββββββββ ββββββββββββββββββββ ββββββββββββββββ
β CritΓ¨res de Validation
Core
- Register + login + refresh + logout fonctionnels
- Refresh token rotation (dΓ©tection rΓ©utilisation)
- CRUD posts avec pagination cursor-based
- Timeline des utilisateurs suivis
- Like/unlike avec contrainte d'unicitΓ©
- Commentaires imbriquΓ©s (post β replies)
- Follow/unfollow self-referential
Production
- Notifications WebSocket en temps rΓ©el
- Emails via BullMQ (welcome + digest)
- Coverage β₯ 85% toutes mΓ©triques
- Dockerfile multi-stage < 200MB
- CI lint+test vert sur chaque PR
- Health check DB + Redis + memory
- Joi validation des variables d'env
Bonus : Ajoutez une recherche full-text sur les posts avec
tsquery PostgreSQL via TypeORM QueryBuilder. Ou implΓ©mentez un systΓ¨me de hashtags avec pages dΓ©diΓ©es (#tag β liste des posts).
Important : Le projet final est notΓ© sur la qualitΓ© globale β code TypeScript strict (pas de
any), gestion d'erreurs cohΓ©rente, tests significatifs (pas de tests triviaux), et architecture modulaire propre.