Comment avoir des variables d'environnement typées avec Zod
Apprends à valider et typer tes variables d'environnement avec Zod pour sécuriser et simplifier l’authentification dans React Router 7.
Pourquoi typer tes variables d’environnement ?
Une app full-stack a besoin de secrets : URL de BDD, clés SMTP, token Stripe... Si l’une de ces valeurs manque ou contient une coquille, ta mise en prod crashe au premier appel réseau - au pire tu exposes tes credentials côté client. La solution ? Valider et typer dès le démarrage grâce à Zod.
Objectifs de la leçon :
- Valider
process.envau boot et provoquer un crash explicite en cas d’oubli. - Profiter de l’autocomplétion TypeScript sur
process.env. - Séparer les variables privées de celles qu’on peut transmettre au navigateur.
- Exposer un hook
useEnv()pour lire les variables « safe » partout côté client.
Définir un schéma Zod pour le serveur
On place toute la logique dans app/server/env.server.ts.
1import { z } from "zod";23const envServerSchema = z.object({4DATABASE_URL: z.string().min(4),5SMTP_HOST: z.string().min(4),6SMTP_PORT: z.coerce.number(),7SMTP_USER: z.string().min(4),8SMTP_PASSWORD:z.string().min(4),9FRONTEND_URL: z.string().url(),10});1112export const serverEnv = envServerSchema.parse(process.env);
parse(process.env)déclenche une erreur détaillée si une clé est absente ou trop courte.z.coerce.number()transforme"465"en465, pratique pour les valeurs numériques reçues en string.
Déclaration de type globale
Pour bénéficier d’un auto-complétion sur process.env, on étend le type
fourni par Node :
1declare global {2namespace NodeJS {3interface ProcessEnv extends z.infer<typeof envServerSchema> {}4}5}
Tu n’écriras plus jamais
process.env.SMTP_HOST ?? "": TypeScript sait que la valeur est présente et de typestring.
Ne jamais envoyer un secret au navigateur
On ne veut pas voir DATABASE_URL ou SMTP_PASSWORD dans les DevTools !
Seule la variable FRONTEND_URL est inoffensive.
On la transmet dans le loader root :
1export async function loader({ request }: LoaderFunctionArgs) {2const user = await getOptionalUser({ request });34return data({5user,6env: {7FRONTEND_URL: serverEnv.FRONTEND_URL, // 🔐 une seule clé autorisée8},9});10}
Un hook useEnv() ultra simple
1import { useRouteLoaderData } from "react-router";23export function useEnv() {4const data = useRouteLoaderData<typeof loader>("root");5return data?.env ?? null; // Renvoie { FRONTEND_URL } ou null6}
Utilisation :
1const { FRONTEND_URL } = useEnv() ?? {};
Aucune dépendance contextuelle, pas de state local : React Router fournit déjà la donnée hydratée depuis le SSR.
Typescript + Zod = fin des mauvaises surprises
1- const url = process.env.DATABSE_URL // 🙀 typo silencieuse2+ const url = serverEnv.DATABASE_URL // 🛑 erreur de compile
Un slug mal orthographié est détecté à la compilation, pas en production.
Crash early plutôt que production cassée
Ajoute volontairement une clé manquante dans .env, puis lance :
1npm run dev
1❌ [env.server] Invalid environment variables:2DATABASE_URL is required
L’app refuse de démarrer – tu corriges avant le déploiement.
C’est bien moins coûteux qu’un Cannot connect to "undefined" reçu par
tes utilisateurs !
Exemple complet
1--- app/server/env.server.ts2import { z } from "zod";34const envServerSchema = z.object({5DATABASE_URL: z.string().url(),6FRONTEND_URL: z.string().url(),7SMTP_HOST: z.string(),8SMTP_PORT: z.coerce.number(),9SMTP_USER: z.string(),10SMTP_PASSWORD:z.string(),11});1213export const serverEnv = envServerSchema.parse(process.env);1415declare global {16namespace NodeJS {17interface ProcessEnv extends z.infer<typeof envServerSchema> {}18}19}20--- app/root.tsx21export async function loader({ request }) {22const user = await getOptionalUser({ request });23return data({24user,25env: { FRONTEND_URL: serverEnv.FRONTEND_URL },26});27}2829export function useEnv() {30const data = useRouteLoaderData<typeof loader>("root");31return data?.env ?? null;32}33--- app/routes/login.tsx34const { FRONTEND_URL } = useEnv() ?? {};35console.log("URL publique :", FRONTEND_URL);
Points clés à retenir
- Zod valide
process.envau boot : mieux vaut un crash immédiat qu’un bug obscur en prod. serverEnvte donne l’autocomplétion et garantit l’existence des clés.- Sépare strictement variables privées (server only) et publiques (envoyées dans le loader).
- Un hook
useEnv()rend les variables publiques accessibles partout côté client, sans context API. - Déclaration globale sur
ProcessEnv= fin des fautes de frappe.
Tip
envServerSchema
immédiatement : tu seras alerté au prochain npm run dev si elle manque.Pour aller plus loin
- Zod documentation – toutes les méthodes de coercition.
- Notre leçon « Création d’un email sécurisé »
montre comment
SMTP_HOST,SMTP_USERetSMTP_PASSWORDsont utilisés pour envoyer un courrier réel.
Comprendre les concepts fondamentaux
Quelle est la principale différence entre les composants client et serveur dans React ?
Optimisation des performances
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Architecture des données
Quel hook permet de gérer les effets de bord dans un composant React ?