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
- loaders et actions Remix
- Forms Remix (docs)
- Validation avec Zod
- Session JWT &
authenticateUser()
Créer la route d’inscription (/register)
- Définir un
loaderpour rediriger un utilisateur déjà connecté. - Définir un
actionpour :- Récupérer et valider les champs via Zod.
- Appeler ton endpoint NestJS (
/auth/register). - Gérer le JWT (via
authenticateUser).
Tip
Préfère schema.safeParse() pour récupérer un objet error sans throw.
app/routes/register.tsx
1import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'2import { json, redirect } from '@remix-run/node'3import { Form, useActionData, useNavigation } from '@remix-run/react'4import { z } from 'zod'5import { getOptionalUser } from '~/auth.server.ts'6import { fetcher } from '~/server/utils.server.ts'7import { authenticateUser } from '~/session.server.ts'8import { AlertFeedback } from '~/components/FeedbackComponent.tsx'910// 1. Schéma Zod pour valider l’input11const registerSchema = z.object({12email: z.string().email({ message: 'Email invalide' }),13password: z14.string().min(6, { message: 'Min 6 caractères' }),15firstName: z16.string().min(2, { message: 'Min 2 caractères' }),17})1819export const loader = async ({ request }: LoaderFunctionArgs) => {20const user = await getOptionalUser({ request })21if (user) {22return redirect('/') // Utilisateur déjà connecté23}24return json({})25}2627export const action = async ({ request }: ActionFunctionArgs) => {28try {29const formData = await request.formData() // ^? formData30const data = Object.fromEntries(formData)31const parsed = registerSchema.safeParse(data)3233if (!parsed.success) {34const msg = parsed.error.errors.map(e => e.message).join(', ')35return json({ error: true, message: msg })36}3738// Appel API NestJS39const res = await fetcher({40url: '/auth/register',41method: 'POST',42data: parsed.data,43request,44})45const { access_token, error, message } = z46.object({47access_token: z.string().optional(),48error: z.boolean().optional(),49message: z.string().optional(),50})51.parse(res)5253if (error) {54return json({ error, message })55}56if (access_token) {57return authenticateUser({ request, userToken: access_token })58}59throw new Error('Erreur inattendue')60} catch (err: any) {61return json({ error: true, message: err.message })62}63}6465export default function RegisterForm() {66const feedback = useActionData()67const nav = useNavigation()68const isLoading = nav.state !== 'idle'6970return (71<Form method="POST" className="space-y-4">72<input73name="firstName"74placeholder="Prénom"75required76disabled={isLoading}77/>78<input79name="email"80type="email"81placeholder="Email"82required83disabled={isLoading}84/>85<input86name="password"87type="password"88placeholder="Mot de passe"89required90disabled={isLoading}91/>92<AlertFeedback feedback={feedback} />93<button type="submit">94{isLoading ? 'Chargement…' : "S'inscrire"}95</button>96</Form>97)98}
Pourquoi ce pattern ?
- Le
loaderprotège la route pour un utilisateur déjà connecté. actiongère la logique “submit” : parse + appel API + session.useNavigation()indique l’état de loading pour désactiver les champs.
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
1import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'2import { json } from '@remix-run/node'3import { Form, useActionData, useLoaderData, useNavigation } from '@remix-run/react'4import { z } from 'zod'5import { getOptionalUser } from '~/auth.server.ts'6import { getConversations } from '~/server/chat.server.ts'7import { getUsers } from '~/server/user.server.ts'8import { fetcher } from '~/server/utils.server.ts'9import { authenticateUser } from '~/session.server.ts'10import { AlertFeedback } from '~/components/FeedbackComponent.tsx'1112// Schéma pour login13const loginSchema = z.object({14email: z.string().email(),15password: z.string(),16})1718export const loader = async ({ request }: LoaderFunctionArgs) => {19const user = await getOptionalUser({ request })20if (user) {21const users = await getUsers({ request })22const convos = await getConversations({ request })23return json({ users, convos })24}25return json({ users: [], convos: [] })26}2728export const action = async ({ request }: ActionFunctionArgs) => {29try {30const formData = await request.formData()31const data = Object.fromEntries(formData)32const parsed = loginSchema.parse(data)3334// Appel direct à l’API NestJS35const response = await fetch(`${process.env.BACKEND_URL}/auth/login`, {36method: 'POST',37headers: { 'Content-Type': 'application/json' },38body: JSON.stringify(parsed),39})40const body = await response.json()41const { access_token, error, message } = z.object({42access_token: z.string().optional(),43error: z.boolean().optional(),44message: z.string().optional(),45}).parse(body)4647if (error) return json({ error, message })48if (access_token) {49return authenticateUser({ request, userToken: access_token })50}51throw new Error('Échec de login')52} catch (err: any) {53return json({ error: true, message: err.message })54}55}5657export default function Index() {58const { users, convos } = useLoaderData()59const feedback = useActionData()60const nav = useNavigation()61const isLoading = nav.state !== 'idle'6263return users.length ? (64<Conversations users={users} conversations={convos} />65) : (66<Form method="POST" className="space-y-4">67<input68name="email"69type="email"70placeholder="Email"71required72disabled={isLoading}73/>74<input75name="password"76type="password"77placeholder="Mot de passe"78required79disabled={isLoading}80/>81<AlertFeedback feedback={feedback} />82<button type="submit">83{isLoading ? '...' : 'Se connecter'}84</button>85</Form>86)87}
Tip
Dans un vrai chat temps réel avec Socket.io, tu pourras lancer la connexion
dans un useEffect après récupération du token en session.
Points clés
- Loader protection : redirige si user déjà connecté.
- Validation serveuse :
zod.safeParse()vszod.parse(). - Session Remix :
authenticateUser()écrit le JWT en cookie
et redirige automatiquement. - Feedback & loading :
useNavigation()etuseActionData(). - 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
- Ajouter
lastNameau formulaire d’inscription- Mets à jour
registerSchemaet la UI. - Assure-toi que l’API accepte ce nouveau champ.
- Mets à jour
- Afficher un lien « Se déconnecter »
- Crée une route
/logoutavec une simple action qui détruit la session. - Ajoute le lien dans la navbar conditionnellement.
- Crée une route
- 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.
- Lorsque l’API renvoie un message d’erreur (ex. email déjà utilisé),
Bon code ! 🚀