Formulaires d’inscription et de connexion avec Remix

Création des formulaires, validation avec Zod, feedback utilisateur et gestion des erreurs.

5 min read

Dans cette leçon, tu vas apprendre à :

  • Créer deux routes Remix pour l’inscription et la connexion.
  • Valider les données côté serveur avec Zod.
  • Appeler ton API NestJS pour générer un JWT.
  • Authentifier l’utilisateur et établir la session.
  • Gérer les feedbacks utilisateur (erreurs & loading).

Cette approche s’appuie sur les notions de


Créer la route d’inscription (/register)

  1. Définir un loader pour rediriger un utilisateur déjà connecté.
  2. Définir un action pour :
    • Récupérer et valider les champs via Zod.
    • Appeler ton endpoint NestJS (/auth/register).
    • Gérer le JWT (via authenticateUser).
app/routes/register.tsx
1
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'
2
import { json, redirect } from '@remix-run/node'
3
import { Form, useActionData, useNavigation } from '@remix-run/react'
4
import { z } from 'zod'
5
import { getOptionalUser } from '~/auth.server.ts'
6
import { fetcher } from '~/server/utils.server.ts'
7
import { authenticateUser } from '~/session.server.ts'
8
import { AlertFeedback } from '~/components/FeedbackComponent.tsx'
9
10
// 1. Schéma Zod pour valider l’input
11
const registerSchema = z.object({
12
email: z.string().email({ message: 'Email invalide' }),
13
password: z
14
.string().min(6, { message: 'Min 6 caractères' }),
15
firstName: z
16
.string().min(2, { message: 'Min 2 caractères' }),
17
})
18
19
export const loader = async ({ request }: LoaderFunctionArgs) => {
20
const user = await getOptionalUser({ request })
21
if (user) {
22
return redirect('/') // Utilisateur déjà connecté
23
}
24
return json({})
25
}
26
27
export const action = async ({ request }: ActionFunctionArgs) => {
28
try {
29
const formData = await request.formData() // ^? formData
30
const data = Object.fromEntries(formData)
31
const parsed = registerSchema.safeParse(data)
32
33
if (!parsed.success) {
34
const msg = parsed.error.errors.map(e => e.message).join(', ')
35
return json({ error: true, message: msg })
36
}
37
38
// Appel API NestJS
39
const res = await fetcher({
40
url: '/auth/register',
41
method: 'POST',
42
data: parsed.data,
43
request,
44
})
45
const { access_token, error, message } = z
46
.object({
47
access_token: z.string().optional(),
48
error: z.boolean().optional(),
49
message: z.string().optional(),
50
})
51
.parse(res)
52
53
if (error) {
54
return json({ error, message })
55
}
56
if (access_token) {
57
return authenticateUser({ request, userToken: access_token })
58
}
59
throw new Error('Erreur inattendue')
60
} catch (err: any) {
61
return json({ error: true, message: err.message })
62
}
63
}
64
65
export default function RegisterForm() {
66
const feedback = useActionData()
67
const nav = useNavigation()
68
const isLoading = nav.state !== 'idle'
69
70
return (
71
<Form method="POST" className="space-y-4">
72
<input
73
name="firstName"
74
placeholder="Prénom"
75
required
76
disabled={isLoading}
77
/>
78
<input
79
name="email"
80
type="email"
81
placeholder="Email"
82
required
83
disabled={isLoading}
84
/>
85
<input
86
name="password"
87
type="password"
88
placeholder="Mot de passe"
89
required
90
disabled={isLoading}
91
/>
92
<AlertFeedback feedback={feedback} />
93
<button type="submit">
94
{isLoading ? 'Chargement…' : "S'inscrire"}
95
</button>
96
</Form>
97
)
98
}

Créer la route de connexion (/)

Cette route affiche soit :

  • La liste des conversations (si connecté)
  • Soit le formulaire de login
app/routes/index.tsx
1
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'
2
import { json } from '@remix-run/node'
3
import { Form, useActionData, useLoaderData, useNavigation } from '@remix-run/react'
4
import { z } from 'zod'
5
import { getOptionalUser } from '~/auth.server.ts'
6
import { getConversations } from '~/server/chat.server.ts'
7
import { getUsers } from '~/server/user.server.ts'
8
import { fetcher } from '~/server/utils.server.ts'
9
import { authenticateUser } from '~/session.server.ts'
10
import { AlertFeedback } from '~/components/FeedbackComponent.tsx'
11
12
// Schéma pour login
13
const loginSchema = z.object({
14
email: z.string().email(),
15
password: z.string(),
16
})
17
18
export const loader = async ({ request }: LoaderFunctionArgs) => {
19
const user = await getOptionalUser({ request })
20
if (user) {
21
const users = await getUsers({ request })
22
const convos = await getConversations({ request })
23
return json({ users, convos })
24
}
25
return json({ users: [], convos: [] })
26
}
27
28
export const action = async ({ request }: ActionFunctionArgs) => {
29
try {
30
const formData = await request.formData()
31
const data = Object.fromEntries(formData)
32
const parsed = loginSchema.parse(data)
33
34
// Appel direct à l’API NestJS
35
const response = await fetch(`${process.env.BACKEND_URL}/auth/login`, {
36
method: 'POST',
37
headers: { 'Content-Type': 'application/json' },
38
body: JSON.stringify(parsed),
39
})
40
const body = await response.json()
41
const { access_token, error, message } = z.object({
42
access_token: z.string().optional(),
43
error: z.boolean().optional(),
44
message: z.string().optional(),
45
}).parse(body)
46
47
if (error) return json({ error, message })
48
if (access_token) {
49
return authenticateUser({ request, userToken: access_token })
50
}
51
throw new Error('Échec de login')
52
} catch (err: any) {
53
return json({ error: true, message: err.message })
54
}
55
}
56
57
export default function Index() {
58
const { users, convos } = useLoaderData()
59
const feedback = useActionData()
60
const nav = useNavigation()
61
const isLoading = nav.state !== 'idle'
62
63
return users.length ? (
64
<Conversations users={users} conversations={convos} />
65
) : (
66
<Form method="POST" className="space-y-4">
67
<input
68
name="email"
69
type="email"
70
placeholder="Email"
71
required
72
disabled={isLoading}
73
/>
74
<input
75
name="password"
76
type="password"
77
placeholder="Mot de passe"
78
required
79
disabled={isLoading}
80
/>
81
<AlertFeedback feedback={feedback} />
82
<button type="submit">
83
{isLoading ? '...' : 'Se connecter'}
84
</button>
85
</Form>
86
)
87
}

Points clés

  • Loader protection : redirige si user déjà connecté.
  • Validation serveuse : zod.safeParse() vs zod.parse().
  • Session Remix : authenticateUser() écrit le JWT en cookie
    et redirige automatiquement.
  • Feedback & loading : useNavigation() et useActionData().
  • Stratégie UI : Remix <Form> + Radix UI + Tailwind CSS
  • API NestJS pour l’authentification (NestJS Auth)

Voir aussi le module suivant pour stocker tes avatars sur AWS S3
et gérer tes dons avec Stripe.


Exercices rapides

  1. Ajouter lastName au formulaire d’inscription
    • Mets à jour registerSchema et la UI.
    • Assure-toi que l’API accepte ce nouveau champ.
  2. Afficher un lien « Se déconnecter »
    • Crée une route /logout avec une simple action qui détruit la session.
    • Ajoute le lien dans la navbar conditionnellement.
  3. Personnaliser les messages d’erreur
    • Lorsque l’API renvoie un message d’erreur (ex. email déjà utilisé),
      affiche une alerte plus détaillée.

Bon code ! 🚀