Créer et lister des conversations entre utilisateurs

API pour créer une conversation, affichage des utilisateurs actifs, historique des discussions.

4 min read

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 ChatService avec méthodes
createConversation et getConversations (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.

src/chat/chat.controller.ts
1
import {
2
Body,
3
Controller,
4
Post,
5
Request,
6
UseGuards,
7
} from '@nestjs/common'
8
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'
9
import { ChatService } from './chat.service'
10
import { CreateConversationDto } from './dto/create-conversation.dto'
11
12
@Controller('chat')
13
export class ChatController {
14
constructor(private readonly chatService: ChatService) {}
15
16
@UseGuards(JwtAuthGuard)
17
@Post()
18
async createConversation(
19
@Body() createConversationDto: CreateConversationDto,
20
@Request() request: RequestWithUser,
21
) {
22
return this.chatService.createConversation({
23
createConversationDto,
24
userId: request.user.userId, // ^? ID de l’utilisateur courant
25
})
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 :

src/chat/dto/create-conversation.dto.ts
1
export class CreateConversationDto {
2
recipientId: string
3
}

Le service NestJS utilise Prisma pour :

  1. Vérifier qu’une conversation n’existe pas déjà
  2. Créer la relation ConversationUser pour les deux utilisateurs
  3. Retourner la conversation initialisée

2. Lister les conversations d’un utilisateur

Pour obtenir toutes les discussions d’un usager, on expose GET /chat :

src/chat/chat.controller.ts
1
@UseGuards(JwtAuthGuard)
2
@Get()
3
async getConversations(@Request() request: RequestWithUser) {
4
return this.chatService.getConversations({
5
userId: request.user.userId, // conversations de cet utilisateur
6
})
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

3. Intégrer dans Remix

Dans Remix, tu vas créer la route app/routes/conversations.tsx
qui contient loader et action.

app/routes/conversations.tsx
1
import { json } from "@remix-run/node"
2
import { useLoaderData, Form } from "@remix-run/react"
3
import type { ActionFunction, LoaderFunction } from "@remix-run/node"
4
5
export const loader: LoaderFunction = async ({ request }) => {
6
const res = await fetch("http://localhost:3000/chat", {
7
headers: { Authorization: request.headers.get("Cookie")! },
8
})
9
return json(await res.json())
10
}
11
12
export const action: ActionFunction = async ({ request }) => {
13
const form = await request.formData()
14
const recipientId = form.get("recipientId")
15
await fetch("http://localhost:3000/chat", {
16
method: "POST",
17
headers: {
18
"Content-Type": "application/json",
19
Authorization: request.headers.get("Cookie")!,
20
},
21
body: JSON.stringify({ recipientId }),
22
})
23
return null
24
}
25
26
export default function ConversationsRoute() {
27
const conversations = useLoaderData()
28
return <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.

app/components/Conversations.tsx
1
import { Form } from "@remix-run/react"
2
import { useOptionalUser } from "~/utils/session"
3
4
export const Conversations = ({
5
conversations,
6
users,
7
}: {
8
conversations: any[]
9
users: { id: string; firstName: string }[]
10
}) => {
11
const connectedUser = useOptionalUser()
12
if (!connectedUser) return null
13
14
// Tous sauf moi
15
const usersExceptMe = users.filter(u => u.id !== connectedUser.id)
16
// Ceux sans conversation
17
const usersWithoutConv = usersExceptMe.filter(
18
u => !conversations.find(c =>
19
c.users.some(u2 => u2.id === u.id),
20
),
21
)
22
23
return (
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
<input
30
type="hidden"
31
name="recipientId"
32
value={user.id}
33
/>
34
<button className="btn">
35
Envoyer un message à {user.firstName}
36
</button>
37
</Form>
38
))}
39
</div>
40
41
<span className="font-bold mt-6">Historique des conversations</span>
42
{conversations.length > 0 ? (
43
conversations.map(conv => (
44
<ConversationItem
45
key={conv.id}
46
conversation={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é.

4. Point clés

  • Le controller NestJS expose POST /chat pour créer, GET /chat pour lister
  • Le DTO CreateConversationDto ne contient que recipientId
  • 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

  1. Étends l’action Remix pour rediriger vers la nouvelle conversation
    après création (utilise redirect de @remix-run/node).
  2. Ajoute un badge “Nouveau” aux conversations sans message lu (simulateur :
    ajoute un champ unread dans le loader).
  3. Implemente un endpoint DELETE /chat/:conversationId pour supprimer
    une conversation depuis NestJS et ajoute le bouton correspondant
    dans le composant Remix.