Créer et lister des conversations entre utilisateurs
API pour créer une conversation, affichage des utilisateurs actifs, historique des discussions.
Dans cette leçon, tu vas apprendre à :
- Créer une conversation privée entre deux utilisateurs côté NestJS
- Lister les conversations d’un utilisateur authentifié
- Intégrer la création et l’affichage des conversations dans ton frontend Remix
- Filtrer les utilisateurs sans conversation et afficher l’historique
Prérequis : authentication JWT en place, service
ChatServiceavec méthodes
createConversationetgetConversations(voir le module précédent).
1. Créer une conversation côté backend
Pour permettre à un utilisateur de démarrer un chat, on définit un endpoint POST /chat
protégé par le guard JwtAuthGuard.
1import {2Body,3Controller,4Post,5Request,6UseGuards,7} from '@nestjs/common'8import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'9import { ChatService } from './chat.service'10import { CreateConversationDto } from './dto/create-conversation.dto'1112@Controller('chat')13export class ChatController {14constructor(private readonly chatService: ChatService) {}1516@UseGuards(JwtAuthGuard)17@Post()18async createConversation(19@Body() createConversationDto: CreateConversationDto,20@Request() request: RequestWithUser,21) {22return this.chatService.createConversation({23createConversationDto,24userId: request.user.userId, // ^? ID de l’utilisateur courant25})26}27}
// @callout: Le guard JwtAuthGuard vérifie que le JWT est valide et
// injecte les infos de l’utilisateur dans request.user.
DTO et logique métier
Le DTO CreateConversationDto contient simplement l’ID du destinataire :
1export class CreateConversationDto {2recipientId: string3}
Le service NestJS utilise Prisma pour :
- Vérifier qu’une conversation n’existe pas déjà
- Créer la relation
ConversationUserpour les deux utilisateurs - Retourner la conversation initialisée
Astuce Prisma
Tu peux utiliser connectOrCreate pour éviter les doublons.
2. Lister les conversations d’un utilisateur
Pour obtenir toutes les discussions d’un usager, on expose GET /chat :
1@UseGuards(JwtAuthGuard)2@Get()3async getConversations(@Request() request: RequestWithUser) {4return this.chatService.getConversations({5userId: request.user.userId, // conversations de cet utilisateur6})7}
Le service remonte chaque conversation avec :
- Les métadonnées (id, timestamps)
- La liste des utilisateurs participants
- Éventuellement le dernier message pour l’aperçu
Tip
Optimise la requête Prisma en sélectionnant uniquement les champs nécessaires.
3. Intégrer dans Remix
Dans Remix, tu vas créer la route app/routes/conversations.tsx
qui contient loader et action.
1import { json } from "@remix-run/node"2import { useLoaderData, Form } from "@remix-run/react"3import type { ActionFunction, LoaderFunction } from "@remix-run/node"45export const loader: LoaderFunction = async ({ request }) => {6const res = await fetch("http://localhost:3000/chat", {7headers: { Authorization: request.headers.get("Cookie")! },8})9return json(await res.json())10}1112export const action: ActionFunction = async ({ request }) => {13const form = await request.formData()14const recipientId = form.get("recipientId")15await fetch("http://localhost:3000/chat", {16method: "POST",17headers: {18"Content-Type": "application/json",19Authorization: request.headers.get("Cookie")!,20},21body: JSON.stringify({ recipientId }),22})23return null24}2526export default function ConversationsRoute() {27const conversations = useLoaderData()28return <Conversations conversations={conversations} />29}
// @callout: Le loader appelle ton API NestJS en passant le cookie JWT.
Composant Conversations
On affiche d’abord les utilisateurs sans discussion, puis l’historique.
1import { Form } from "@remix-run/react"2import { useOptionalUser } from "~/utils/session"34export const Conversations = ({5conversations,6users,7}: {8conversations: any[]9users: { id: string; firstName: string }[]10}) => {11const connectedUser = useOptionalUser()12if (!connectedUser) return null1314// Tous sauf moi15const usersExceptMe = users.filter(u => u.id !== connectedUser.id)16// Ceux sans conversation17const usersWithoutConv = usersExceptMe.filter(18u => !conversations.find(c =>19c.users.some(u2 => u2.id === u.id),20),21)2223return (24<div className="flex flex-col gap-y-4 max-w-md mx-auto">25<span className="font-bold text-sm">Utilisateurs actifs</span>26<div className="flex flex-col gap-2">27{usersWithoutConv.map(user => (28<Form method="post" key={user.id}>29<input30type="hidden"31name="recipientId"32value={user.id}33/>34<button className="btn">35Envoyer un message à {user.firstName}36</button>37</Form>38))}39</div>4041<span className="font-bold mt-6">Historique des conversations</span>42{conversations.length > 0 ? (43conversations.map(conv => (44<ConversationItem45key={conv.id}46conversation={conv}47/>48))49) : (50<div>Aucune conversation n'a été créée.</div>51)}52</div>53)54}
// @callout: On utilise useOptionalUser (session Remix) pour récupérer
// l’utilisateur connecté.
Performance
Si tu as beaucoup d’utilisateurs, charge-les paginés ou via une requête séparée.
4. Point clés
- Le controller NestJS expose POST /chat pour créer, GET /chat pour lister
- Le DTO
CreateConversationDtone contient querecipientId - Remix utilise loader pour fetcher et action pour poster
- On filtre les utilisateurs sans conversation avec un simple
.filter - UI React + Tailwind : composants modulaires
<Conversations>,<ConversationItem>
Exercices
- Étends l’action Remix pour rediriger vers la nouvelle conversation
après création (utiliseredirectde@remix-run/node). - Ajoute un badge “Nouveau” aux conversations sans message lu (simulateur :
ajoute un champunreaddans le loader). - Implemente un endpoint DELETE
/chat/:conversationIdpour supprimer
une conversation depuis NestJS et ajoute le bouton correspondant
dans le composant Remix.