Utiliser Firebase avec Next.js

Utiliser Firebase avec Next.js

Quand on commence à construire une application web sérieuse avec Next.js, il arrive presque toujours un moment où l’on se pose la même question : comment gérer proprement l’authentification, la base de données, le stockage de fichiers, les notifications ou encore la synchronisation temps réel sans passer des semaines à tout réinventer ? C’est souvent là que Firebase entre en scène. Et ce n’est pas un hasard. Firebase permet d’aller vite, de rester productif, et de proposer une expérience utilisateur très fluide, tout en gardant une architecture relativement simple à maintenir. Associé à Next.js, qui apporte le rendu côté serveur, le routage moderne, l’optimisation des performances et une très bonne expérience développeur, on obtient un duo particulièrement solide pour lancer un produit web sans se perdre dans la complexité inutile.

Ce qui rend cette combinaison intéressante, ce n’est pas seulement la facilité de démarrage. C’est aussi la manière dont elle accompagne l’évolution d’un projet. On peut commencer par une authentification simple, une petite base Firestore et un formulaire d’upload, puis ajouter progressivement des règles de sécurité, des données structurées, des pages protégées, du SSR, des fonctions server-side et des optimisations plus avancées. Le tout sans tout casser à chaque étape. En pratique, c’est ce que cherchent beaucoup d’équipes et de développeurs indépendants : une base simple, rapide à mettre en place, mais assez robuste pour grandir avec le produit.

Dans cet article, on va aller loin, mais sans perdre le côté concret. On va voir comment configurer Firebase dans un projet Next.js, comment gérer l’authentification avec des comptes utilisateurs réels, comment lire et écrire dans Firestore, comment gérer le stockage de fichiers, comment protéger des pages, comment manipuler les variables d’environnement, et comment éviter les pièges les plus fréquents. On prendra aussi le temps de parler des bonnes pratiques qui font la différence entre une intégration “qui marche” et une intégration propre, sécurisée et agréable à maintenir dans la durée.

Pourquoi choisir Firebase avec Next.js ?

Il existe beaucoup de stacks possibles pour construire une application web moderne, mais Firebase + Next.js a un avantage très important : il réduit considérablement le temps nécessaire pour obtenir une première version fonctionnelle, tout en gardant un haut niveau de qualité côté expérience utilisateur. Next.js apporte une structure solide autour de React avec des fonctionnalités comme le routage par fichiers, les Server Components, les API routes, le rendu hybride et l’optimisation des performances. Firebase, de son côté, fournit des briques prêtes à l’emploi pour l’authentification, la base de données NoSQL, le stockage objet, les fonctions serverless et d’autres services managés.

Quand on combine les deux, on peut très vite développer des produits comme des dashboards, des outils SaaS, des plateformes d’abonnement, des applications de réservation, des systèmes internes, des forums, des marketplaces ou des MVP destinés à valider une idée rapidement. La logique est simple : Next.js structure l’application, Firebase gère une grande partie de l’infrastructure backend. Pour une équipe réduite ou un développeur solo, c’est un gain énorme.

Il y a aussi un aspect humain qu’on sous-estime souvent. Quand on travaille avec des outils qui permettent d’avancer sans lutter contre la technique à chaque étape, on garde de l’énergie pour ce qui compte vraiment : le produit, les utilisateurs et les cas d’usage. C’est très différent d’une architecture trop lourde où chaque petit changement demande une chaîne interminable de configurations. Avec Firebase et Next.js, on garde l’impression de construire, pas de se battre.

Ce que Firebase apporte concrètement à Next.js

Firebase n’est pas juste “une base de données”. C’est un ensemble de services managés. Les plus utiles dans le contexte Next.js sont souvent Authentication, Firestore, Storage et, selon les cas, Cloud Functions. Authentication permet de gérer les comptes utilisateurs avec différents fournisseurs comme email/mot de passe, Google, GitHub, Facebook et d’autres. Firestore sert à stocker des documents structurés et à les lire en temps réel. Storage permet d’envoyer et d’héberger des fichiers comme des images, des PDF ou des avatars. Cloud Functions permet d’exécuter du code côté serveur dans des événements précis.

Pour Next.js, cela signifie que l’on peut construire une application avec une vraie logique métier sans devoir tout coder soi-même. On peut par exemple utiliser Firebase Auth pour l’inscription et la connexion, Firestore pour enregistrer le profil utilisateur, Next.js pour le rendu des pages et les actions serveur, Storage pour les images de profil, et des règles de sécurité Firebase pour garantir que chaque utilisateur ne voit que ses propres données.

Cette séparation des responsabilités est intéressante. Next.js s’occupe de l’interface, de la navigation, du rendu et de la logique côté serveur quand c’est utile. Firebase s’occupe de services backend managés. Résultat : une architecture lisible, souvent plus facile à faire évoluer qu’un backend monolithique bricolé rapidement.

Préparer le projet Next.js

Avant d’intégrer Firebase, il faut un projet Next.js propre. La version moderne de Next.js utilise généralement l’App Router, même si certaines bases de code utilisent encore le Pages Router. Ici, on va partir sur une approche moderne avec App Router, parce que c’est celle qui correspond le mieux aux projets actuels.

On crée d’abord l’application :

npx create-next-app@latest my-firebase-app
cd my-firebase-app

Ensuite, on installe le SDK Firebase :

npm install firebase

Selon la manière dont vous comptez gérer l’authentification côté serveur, vous pourrez aussi avoir besoin du SDK Admin Firebase :

npm install firebase-admin

Dans un projet sérieux, il est important de distinguer le SDK client Firebase du SDK admin. Le SDK client est destiné au navigateur et respecte les règles de sécurité Firestore et Storage. Le SDK admin, lui, a des privilèges élevés et doit rester strictement côté serveur. Mélanger les deux sans réfléchir peut créer de vraies failles de sécurité. Cette distinction est l’un des points les plus importants de toute intégration Firebase + Next.js.

Créer un projet Firebase

Une fois l’application Next.js prête, il faut créer le projet Firebase dans la console Firebase. On y ajoute une application web, puis on récupère les clés de configuration du SDK client. Ces informations ne sont pas des secrets au sens strict, mais elles doivent tout de même être gérées proprement avec des variables d’environnement.

Dans la majorité des cas, vous allez obtenir des valeurs comme :

NEXT_PUBLIC_FIREBASE_API_KEY=...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=...
NEXT_PUBLIC_FIREBASE_PROJECT_ID=...
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=...
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=...
NEXT_PUBLIC_FIREBASE_APP_ID=...

Le préfixe NEXT_PUBLIC_ est important. Il indique que ces variables peuvent être exposées au navigateur. C’est logique pour les paramètres de configuration Firebase côté client. En revanche, les identifiants d’administration ou les clés privées du service account ne doivent jamais être exposés au client.

Initialiser Firebase proprement dans Next.js

Une bonne pratique consiste à centraliser l’initialisation Firebase dans un fichier dédié. Cela évite les doubles initialisations et rend l’architecture plus claire.

Par exemple, dans src/lib/firebase.ts :

import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};

const app = getApps().length ? getApp() : initializeApp(firebaseConfig);

export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);
export default app;

Ce petit fichier a l’air simple, mais il est essentiel. Il évite que Firebase soit initialisé plusieurs fois pendant le rendu ou lors du hot reload en développement. Sans cette précaution, on peut rencontrer des comportements étranges et difficiles à diagnostiquer.

Gérer l’authentification avec Firebase Auth

L’authentification est souvent la première fonctionnalité qu’on souhaite mettre en place. Dans une application moderne, on veut généralement proposer l’inscription par email/mot de passe, la connexion, la déconnexion, et parfois des fournisseurs externes comme Google.

Firebase Auth simplifie beaucoup ce travail. Voici un exemple de service pour inscrire un utilisateur :

import { createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut } from "firebase/auth";
import { auth } from "@/lib/firebase";

export async function registerWithEmail(email: string, password: string) {
  const result = await createUserWithEmailAndPassword(auth, email, password);
  return result.user;
}

export async function loginWithEmail(email: string, password: string) {
  const result = await signInWithEmailAndPassword(auth, email, password);
  return result.user;
}

export async function logout() {
  await signOut(auth);
}

Dans la pratique, vous allez souvent créer un formulaire React qui appelle ces fonctions. Voici un exemple de composant client :

"use client";

import { useState } from "react";
import { registerWithEmail } from "@/services/auth";

export default function RegisterForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setMessage("");

    try {
      await registerWithEmail(email, password);
      setMessage("Compte créé avec succès.");
    } catch (error: any) {
      setMessage(error.message || "Une erreur est survenue.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        className="border p-2 w-full"
      />
      <input
        type="password"
        placeholder="Mot de passe"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        className="border p-2 w-full"
      />
      <button disabled={loading} className="bg-black text-white px-4 py-2 rounded">
        {loading ? "Création..." : "Créer le compte"}
      </button>
      {message && <p>{message}</p>}
    </form>
  );
}

Ce n’est qu’une base, mais elle montre la logique essentielle. On récupère les informations du formulaire, on appelle Firebase Auth, puis on gère le retour utilisateur de manière claire. Dans une vraie application, on ajouterait aussi la validation, la gestion des erreurs plus détaillée et probablement une meilleure interface.

Ajouter la connexion avec Google

Beaucoup d’applications gagnent à proposer une connexion Google. Cela réduit la friction à l’inscription et améliore souvent le taux de conversion. Avec Firebase, l’intégration est assez simple.

import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import { auth } from "@/lib/firebase";

const provider = new GoogleAuthProvider();

export async function loginWithGoogle() {
  const result = await signInWithPopup(auth, provider);
  return result.user;
}

Et côté interface :

"use client";

import { loginWithGoogle } from "@/services/auth";

export default function GoogleLoginButton() {
  return (
    <button
      onClick={async () => {
        await loginWithGoogle();
      }}
      className="border px-4 py-2 rounded"
    >
      Continuer avec Google
    </button>
  );
}

Dans beaucoup de cas, la connexion sociale accélère l’adoption de l’application. L’utilisateur n’a pas besoin de créer un nouveau mot de passe, et vous réduisez aussi les abandons pendant l’inscription.

Lire et écrire des données avec Firestore

Firestore est probablement le service Firebase le plus utilisé dans les projets Next.js. C’est une base de données NoSQL orientée documents, très pratique pour stocker des profils, des articles, des tâches, des commentaires, des commandes ou des messages. Son modèle de données n’est pas relationnel, et c’est important à garder en tête. Il faut penser en collections et documents, pas en tables et jointures.

Par exemple, si vous créez une application de gestion de notes, vous pouvez avoir une collection notes dans laquelle chaque document contient le titre, le contenu, l’auteur, la date de création et un statut.

Pour écrire un document :

import { addDoc, collection, serverTimestamp } from "firebase/firestore";
import { db } from "@/lib/firebase";

export async function createNote(userId: string, title: string, content: string) {
  const docRef = await addDoc(collection(db, "notes"), {
    userId,
    title,
    content,
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp(),
  });

  return docRef.id;
}

Pour lire les notes d’un utilisateur :

import { collection, getDocs, query, where, orderBy } from "firebase/firestore";
import { db } from "@/lib/firebase";

export async function getUserNotes(userId: string) {
  const q = query(
    collection(db, "notes"),
    where("userId", "==", userId),
    orderBy("createdAt", "desc")
  );

  const snapshot = await getDocs(q);

  return snapshot.docs.map((doc) => ({
    id: doc.id,
    ...doc.data(),
  }));
}

Firestore est très agréable à utiliser, mais il faut respecter quelques principes. D’abord, il vaut mieux structurer les données en pensant aux requêtes que l’on devra faire. Ensuite, il faut surveiller les coûts, car les lectures fréquentes peuvent vite s’accumuler. Enfin, il faut garder à l’esprit que les règles de sécurité doivent être bien écrites, sinon n’importe quel client pourrait potentiellement lire des données qu’il ne devrait pas voir.

Afficher les données dans une page Next.js

Dans Next.js, on peut récupérer les données côté client ou côté serveur selon le besoin. Avec Firebase, beaucoup de projets utilisent d’abord une récupération côté client, surtout pour les données temps réel ou les dashboards interactifs.

Voici un exemple simple de page client qui affiche les notes :

"use client";

import { useEffect, useState } from "react";
import { getUserNotes } from "@/services/notes";

type Note = {
  id: string;
  title: string;
  content: string;
};

export default function NotesPage() {
  const [notes, setNotes] = useState<Note[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadNotes() {
      const data = await getUserNotes("user-id-example");
      setNotes(data as Note[]);
      setLoading(false);
    }

    loadNotes();
  }, []);

  if (loading) return <p>Chargement...</p>;

  return (
    <div className="space-y-4">
      {notes.map((note) => (
        <article key={note.id} className="border rounded p-4">
          <h2 className="text-xl font-bold">{note.title}</h2>
          <p>{note.content}</p>
        </article>
      ))}
    </div>
  );
}

Dans un projet réel, on remplacerait évidemment "user-id-example" par l’identifiant authentifié de l’utilisateur courant. On peut récupérer cet identifiant via Firebase Auth côté client ou bien via une session sécurisée côté serveur.

Gérer l’état d’authentification dans l’application

Une application utile n’affiche pas seulement un formulaire de connexion. Elle doit aussi savoir en permanence si un utilisateur est connecté, qui il est, et quelles pages il peut voir. Le gestionnaire d’état d’authentification est donc une pièce centrale.

Firebase propose un observateur qui permet de suivre l’état de l’utilisateur en temps réel :

import { onAuthStateChanged, User } from "firebase/auth";
import { auth } from "@/lib/firebase";

export function subscribeToAuthChanges(callback: (user: User | null) => void) {
  return onAuthStateChanged(auth, callback);
}

Ensuite, dans un composant global :

"use client";

import { useEffect, useState } from "react";
import { User } from "firebase/auth";
import { subscribeToAuthChanges } from "@/services/authState";

export default function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [initializing, setInitializing] = useState(true);

  useEffect(() => {
    const unsubscribe = subscribeToAuthChanges((currentUser) => {
      setUser(currentUser);
      setInitializing(false);
    });

    return unsubscribe;
  }, []);

  if (initializing) {
    return <p>Initialisation...</p>;
  }

  return <>{children}</>;
}

Cette logique est souvent indispensable pour éviter le fameux effet de “flash” où la page affiche brièvement du contenu non autorisé avant de le masquer. En matière d’expérience utilisateur, ce petit détail change beaucoup.

Protéger les routes et les pages privées

La sécurité ne doit pas reposer uniquement sur le rendu d’interface. C’est une erreur classique. Même si vous cachez un bouton ou une page dans le front-end, cela ne protège pas réellement les données. Il faut aussi sécuriser les accès aux données avec Firestore Rules et, quand c’est nécessaire, des vérifications côté serveur.

Pour les pages privées, on peut utiliser un composant de garde simple :

"use client";

import { useEffect, useState } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "@/lib/firebase";
import { useRouter } from "next/navigation";

export default function ProtectedPage({ children }: { children: React.ReactNode }) {
  const [checked, setChecked] = useState(false);
  const router = useRouter();

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      if (!user) {
        router.push("/login");
      } else {
        setChecked(true);
      }
    });

    return unsubscribe;
  }, [router]);

  if (!checked) return <p>Vérification de votre session...</p>;

  return <>{children}</>;
}

C’est utile, mais il faut aller plus loin. Le vrai verrouillage se fait dans les règles Firestore.

Écrire des règles Firestore solides

Les règles de sécurité sont l’une des parties les plus importantes de Firebase. Elles définissent qui peut lire, créer, modifier ou supprimer des documents. Une bonne règle de sécurité peut sauver votre application. Une mauvaise règle peut l’exposer.

Exemple de règles pour une collection notes :

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /notes/{noteId} {
      allow create: if request.auth != null
                    && request.resource.data.userId == request.auth.uid;

      allow read: if request.auth != null
                   && resource.data.userId == request.auth.uid;

      allow update: if request.auth != null
                    && resource.data.userId == request.auth.uid;

      allow delete: if request.auth != null
                    && resource.data.userId == request.auth.uid;
    }
  }
}

L’idée est simple : un utilisateur ne peut accéder qu’aux documents dont il est propriétaire. Dans les applications réelles, les règles peuvent devenir plus complexes, mais ce principe de base reste extrêmement important.

Il faut aussi tester ces règles. Beaucoup de développeurs les rédigent rapidement puis les oublient. C’est risqué. Une règle un peu trop permissive peut transformer une application privée en base de données ouverte.

Utiliser le temps réel de Firestore

L’un des points forts de Firebase, c’est le temps réel. Quand des données changent, l’interface peut se mettre à jour automatiquement sans rechargement manuel. C’est très agréable pour des messages, des commentaires, des tableaux de bord ou des systèmes de collaboration.

Voici un exemple avec onSnapshot :

import { collection, onSnapshot, query, where } from "firebase/firestore";
import { db } from "@/lib/firebase";

export function subscribeToUserNotes(userId: string, callback: (notes: any[]) => void) {
  const q = query(collection(db, "notes"), where("userId", "==", userId));

  return onSnapshot(q, (snapshot) => {
    const notes = snapshot.docs.map((doc) => ({
      id: doc.id,
      ...doc.data(),
    }));
    callback(notes);
  });
}

Et dans un composant :

"use client";

import { useEffect, useState } from "react";
import { subscribeToUserNotes } from "@/services/notesRealtime";

export default function LiveNotes() {
  const [notes, setNotes] = useState<any[]>([]);

  useEffect(() => {
    const unsubscribe = subscribeToUserNotes("user-id-example", setNotes);
    return unsubscribe;
  }, []);

  return (
    <div>
      {notes.map((note) => (
        <div key={note.id}>{note.title}</div>
      ))}
    </div>
  );
}

Le temps réel a un vrai pouvoir produit. Il donne une sensation de vivacité à l’application. L’utilisateur a l’impression que tout répond instantanément, et cela renforce la qualité perçue de votre interface.

Gérer les fichiers avec Firebase Storage

Très souvent, une application a besoin d’upload des fichiers. Cela peut être une photo de profil, une image de produit, une facture PDF ou un document utilisateur. Firebase Storage simplifie cette partie.

Voici un exemple d’upload :

import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { storage } from "@/lib/firebase";

export async function uploadAvatar(file: File, userId: string) {
  const storageRef = ref(storage, `avatars/${userId}/${file.name}`);
  await uploadBytes(storageRef, file);
  const downloadURL = await getDownloadURL(storageRef);
  return downloadURL;
}

Et un composant d’upload simple :

"use client";

import { useState } from "react";
import { uploadAvatar } from "@/services/storage";

export default function AvatarUpload() {
  const [uploading, setUploading] = useState(false);
  const [url, setUrl] = useState("");

  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    setUploading(true);
    try {
      const downloadURL = await uploadAvatar(file, "user-id-example");
      setUrl(downloadURL);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div className="space-y-4">
      <input type="file" onChange={handleFileChange} />
      {uploading && <p>Téléversement en cours...</p>}
      {url && <img src={url} alt="Avatar" width={120} height={120} />}
    </div>
  );
}

Comme pour Firestore, il faut aussi sécuriser Storage avec des règles. Cela évite qu’un utilisateur puisse lire ou remplacer les fichiers des autres.

Règles Storage pour limiter l’accès

Exemple de règles simples :

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /avatars/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;
    }
  }
}

Encore une fois, la logique est claire : chacun n’accède qu’à son propre espace de stockage. C’est simple à exprimer, et c’est précisément ce qui rend Firebase pratique. Mais il faut vraiment prendre le temps d’écrire ces règles avec soin.

Utiliser Firebase côté serveur dans Next.js

Next.js ne se limite pas au navigateur. On peut aussi faire beaucoup de choses côté serveur. Cela devient intéressant quand on veut vérifier une session, protéger des routes, générer des pages avec des données sécurisées ou utiliser le SDK admin.

Le SDK admin Firebase est généralement utilisé dans des fichiers côté serveur, par exemple dans une route API ou une Server Action.

Exemple d’initialisation admin :

import admin from "firebase-admin";

if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert({
      projectId: process.env.FIREBASE_PROJECT_ID,
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
    }),
  });
}

export const adminAuth = admin.auth();
export const adminDb = admin.firestore();

Là encore, on ne met jamais ces variables dans le client. Elles doivent rester privées. C’est une règle fondamentale.

Le SDK admin permet par exemple de vérifier un token ID transmis par le client :

import { NextRequest, NextResponse } from "next/server";
import { adminAuth } from "@/lib/firebase-admin";

export async function GET(req: NextRequest) {
  const authHeader = req.headers.get("authorization");
  const token = authHeader?.replace("Bearer ", "");

  if (!token) {
    return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
  }

  try {
    const decoded = await adminAuth.verifyIdToken(token);
    return NextResponse.json({ uid: decoded.uid });
  } catch {
    return NextResponse.json({ error: "Token invalide" }, { status: 401 });
  }
}

Cette approche devient utile quand vous avez besoin d’une vraie validation serveur, par exemple pour des actions sensibles, des abonnements, des données critiques ou des permissions avancées.

Les Server Actions et Firebase

Avec les versions modernes de Next.js, les Server Actions peuvent être très pratiques. Elles permettent d’exécuter une action côté serveur directement depuis un composant. C’est particulièrement intéressant si vous voulez enregistrer des données, vérifier une session ou appeler Firebase Admin de manière sécurisée.

Exemple :

"use server";

import { adminDb } from "@/lib/firebase-admin";

export async function createProfile(uid: string, displayName: string) {
  await adminDb.collection("profiles").doc(uid).set({
    displayName,
    updatedAt: new Date(),
  });
}

Les Server Actions peuvent simplifier le code si elles sont utilisées avec discernement. Elles ne remplacent pas tout, mais elles peuvent rendre certaines interactions beaucoup plus propres.

Utiliser Firebase avec le rendu serveur de Next.js

Next.js permet aussi de rendre des pages côté serveur avec des données. Cela peut être utile pour le SEO, pour améliorer le premier affichage ou pour charger du contenu avant l’arrivée du JavaScript client.

Avec Firebase Admin, on peut récupérer des données côté serveur :

import { adminDb } from "@/lib/firebase-admin";

export default async function BlogPage() {
  const snapshot = await adminDb.collection("posts").get();
  const posts = snapshot.docs.map((doc) => ({
    id: doc.id,
    ...doc.data(),
  }));

  return (
    <main>
      <h1>Articles</h1>
      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </main>
  );
}

Cette technique est particulièrement utile pour les pages publiques, les pages marketing, les blogs ou les listes de contenu indexables. On bénéficie ainsi à la fois de la puissance de Firebase et des avantages SEO de Next.js.

Séparer proprement les couches

Une erreur fréquente consiste à mélanger la logique Firebase directement dans les composants UI. Cela marche au début, mais devient vite difficile à maintenir. Une meilleure approche consiste à séparer les responsabilités : un dossier pour les services Firebase, un dossier pour les hooks, un dossier pour les composants, un dossier pour les types. Cette discipline évite beaucoup de confusion quand le projet grossit.

Par exemple :

src/
  lib/
    firebase.ts
    firebase-admin.ts
  services/
    auth.ts
    notes.ts
    storage.ts
  components/
    RegisterForm.tsx
    GoogleLoginButton.tsx
    ProtectedPage.tsx
  app/
    login/
    dashboard/

Ce n’est pas une règle absolue, mais une structure comme celle-ci aide énormément à garder l’esprit clair. On sait où chercher quoi. Et quand une autre personne rejoint le projet, elle ne perd pas une journée à comprendre où se cache la logique métier.

Validation des données avant envoi

Firebase ne remplace pas la validation. Même si l’infrastructure est managée, votre application a quand même besoin de contrôler ce qui entre dans le système. Il est préférable de vérifier les champs côté client pour améliorer l’expérience, puis côté serveur ou via les règles pour assurer la sécurité.

Exemple de validation simple avant création d’une note :

export function validateNoteInput(title: string, content: string) {
  if (!title.trim()) {
    throw new Error("Le titre est obligatoire.");
  }

  if (title.length > 100) {
    throw new Error("Le titre ne doit pas dépasser 100 caractères.");
  }

  if (!content.trim()) {
    throw new Error("Le contenu est obligatoire.");
  }

  return true;
}

Puis on l’utilise avant l’écriture Firestore :

import { validateNoteInput } from "@/services/validation";
import { createNote } from "@/services/notes";

async function handleCreate() {
  validateNoteInput(title, content);
  await createNote(userId, title, content);
}

Ces vérifications paraissent élémentaires, mais elles évitent beaucoup de bugs inutiles et améliorent la cohérence des données. Une bonne application n’est pas seulement jolie, elle est aussi stable.

Gérer les erreurs avec élégance

Les erreurs Firebase peuvent être déroutantes au début, surtout parce que certains messages sont très techniques. Il est important de les transformer en messages compréhensibles pour l’utilisateur. Par exemple, un mot de passe trop faible ou un email déjà utilisé ne devraient pas s’afficher comme des codes obscurs.

Exemple :

export function getFriendlyAuthError(error: any) {
  switch (error.code) {
    case "auth/email-already-in-use":
      return "Cet email est déjà utilisé.";
    case "auth/weak-password":
      return "Le mot de passe est trop faible.";
    case "auth/invalid-email":
      return "L'adresse email est invalide.";
    case "auth/user-not-found":
      return "Aucun compte trouvé avec cet email.";
    case "auth/wrong-password":
      return "Mot de passe incorrect.";
    default:
      return "Une erreur est survenue. Veuillez réessayer.";
  }
}

Dans une interface sérieuse, ce genre de traitement améliore énormément la qualité perçue. L’utilisateur ne se sent pas bloqué par un message incompréhensible. Il sait quoi faire ensuite.

Ajouter des notifications ou des événements

Selon votre projet, vous voudrez peut-être créer un système d’événements. Firebase peut aider à stocker des activités utilisateur, des logs métier ou des notifications. Par exemple, chaque fois qu’un utilisateur crée une note, vous pouvez ajouter un événement dans une collection activity.

import { addDoc, collection, serverTimestamp } from "firebase/firestore";
import { db } from "@/lib/firebase";

export async function logActivity(userId: string, action: string) {
  await addDoc(collection(db, "activity"), {
    userId,
    action,
    createdAt: serverTimestamp(),
  });
}

Ce type de journal peut ensuite servir pour afficher un fil d’activité, un historique ou des notifications internes. Beaucoup d’applications ont besoin, tôt ou tard, d’une vue “ce qui s’est passé”. Firebase est bien adapté à ça.

Bonnes pratiques de performance

Quand on utilise Firebase avec Next.js, il faut aussi penser performance. Par exemple, éviter les lectures inutiles, limiter les écoutes en temps réel aux écrans qui en ont besoin, paginer les gros ensembles de données, et ne pas charger Firebase dans des pages où il n’est pas utile. On peut aussi tirer parti du code splitting naturel de Next.js pour séparer les parties client et serveur.

Un point important est la pagination Firestore. Si vous avez beaucoup de documents, il vaut mieux charger les résultats par lots :

import { collection, query, orderBy, limit, getDocs } from "firebase/firestore";
import { db } from "@/lib/firebase";

export async function getRecentPosts() {
  const q = query(collection(db, "posts"), orderBy("createdAt", "desc"), limit(10));
  const snapshot = await getDocs(q);

  return snapshot.docs.map((doc) => ({
    id: doc.id,
    ...doc.data(),
  }));
}

Si votre application devient plus grande, cette attention aux détails devient essentielle. La vitesse ne dépend pas seulement de Next.js ou Firebase séparément, mais de la façon dont on les utilise ensemble.

Déployer une application Next.js + Firebase

Le déploiement est souvent l’étape où les projets se cassent s’ils n’ont pas été préparés correctement. Heureusement, Next.js se déploie très bien sur des plateformes comme Vercel, et Firebase reste accessible depuis le client ou le serveur selon vos besoins. Ce qui compte, c’est surtout la gestion des variables d’environnement, la cohérence entre les environnements de développement et de production, et les règles de sécurité.

Dans Vercel, il faudra configurer les variables d’environnement dans les paramètres du projet. Dans Firebase, il faudra peut-être aussi activer les domaines autorisés pour l’authentification, surtout si vous utilisez Google Sign-In ou d’autres providers externes.

Il faut également s’assurer que les règles Firestore et Storage sont bien publiées sur l’environnement de production. Beaucoup de développeurs testent localement avec des règles permissives, puis oublient de les durcir avant le déploiement. C’est une erreur courante et coûteuse.

Tester avant de livrer

On ne le répètera jamais assez : testez vos flux principaux. Inscription, connexion, lecture des données, création, modification, suppression, upload, erreurs d’authentification, permissions. Firebase simplifie énormément les opérations quotidiennes, mais il ne remplace pas une vraie stratégie de test.

Au minimum, il faut vérifier que :

  • l’utilisateur connecté voit uniquement ses données ;

  • un utilisateur non connecté ne peut pas accéder aux pages privées ;

  • les uploads refusent les formats non autorisés ;

  • les erreurs Firebase sont affichées de manière claire ;

  • les règles Firestore et Storage bloquent les accès illégitimes.

Même sans suite de tests complète au départ, cette vérification manuelle rigoureuse évite les mauvaises surprises.

Cas d’usage concrets

Firebase avec Next.js devient particulièrement puissant dans certains scénarios. Une application de gestion de tâches peut utiliser Auth pour les comptes, Firestore pour les tâches, et Storage pour les pièces jointes. Une plateforme de formation peut utiliser Auth pour les étudiants, Firestore pour les cours et les progrès, Storage pour les vidéos ou supports PDF. Un mini-SaaS peut utiliser Firestore pour les organisations et les abonnements, puis Next.js pour les tableaux de bord. Une application interne peut s’appuyer sur les règles Firestore pour compartimenter les équipes et les rôles.

Ce qui est intéressant, c’est qu’on peut bâtir des produits très différents avec la même base. Cette réutilisabilité donne une vraie cohérence à l’équipe technique. On apprend une stack, puis on la réemploie avec de petites variations plutôt que de repartir de zéro à chaque fois.

Les erreurs fréquentes à éviter

Beaucoup de problèmes reviennent souvent. Première erreur : mettre des secrets serveur dans des variables NEXT_PUBLIC_. Deuxième erreur : oublier les règles de sécurité et compter uniquement sur l’interface. Troisième erreur : initialiser Firebase plusieurs fois. Quatrième erreur : mélanger trop de logique métier dans les composants. Cinquième erreur : ne pas séparer clairement le code client du code serveur. Sixième erreur : ignorer la pagination et charger trop de données. Septième erreur : ne pas gérer les états de chargement et d’erreur dans l’interface.

En pratique, ce sont rarement les grandes architectures qui font échouer un projet, mais plutôt l’accumulation de petits oublis. Un projet Next.js + Firebase bien construit n’a pas besoin d’être compliqué. Il a besoin d’être propre, cohérent et soigneusement sécurisé.

Une architecture simple, mais sérieuse

Voici une façon de penser l’architecture. Next.js est la couche d’expérience utilisateur et de rendu. Firebase Auth gère l’identité. Firestore stocke les données de l’application. Storage stocke les fichiers. Firebase Admin intervient seulement côté serveur pour les actions sensibles. Les règles de sécurité filtrent les accès. Les composants UI restent concentrés sur l’affichage et les interactions. Les services contiennent la logique métier Firebase. Cette répartition donne une base claire, lisible et durable.

Quand cette architecture est en place, on peut avancer vite sans avoir l’impression de construire sur du sable. C’est probablement l’un des plus grands bénéfices de Firebase avec Next.js : on gagne en vitesse de développement sans forcément sacrifier la qualité, à condition de rester discipliné.

Exemple de mini flux complet

Pour terminer la partie technique, prenons un mini scénario très concret. Un utilisateur s’inscrit, crée un profil, ajoute une note, puis téléverse un avatar. Voici la logique générale :

// Inscription
const user = await registerWithEmail(email, password);

// Création du profil
await createProfile(user.uid, displayName);

// Création d'une note
await createNote(user.uid, "Ma première note", "Contenu de la note");

// Upload avatar
const avatarUrl = await uploadAvatar(file, user.uid);

Et côté Firestore, vous aurez quelque chose comme :

{
  "profiles": {
    "uid123": {
      "displayName": "Hassan",
      "avatarUrl": "https://..."
    }
  },
  "notes": [
    {
      "userId": "uid123",
      "title": "Ma première note",
      "content": "Contenu de la note"
    }
  ]
}

Bien sûr, dans une vraie application, vous structurerez cela de manière plus sophistiquée. Mais ce petit enchaînement illustre parfaitement la simplicité du duo Firebase + Next.js : une interaction utilisateur, quelques appels bien ciblés, et l’application prend vie rapidement.

Conclusion

Utiliser Firebase avec Next.js, ce n’est pas seulement brancher un backend “facile” sur un frontend moderne. C’est choisir une manière de construire qui privilégie la vitesse, la clarté et la capacité à évoluer sans douleur inutile. Next.js apporte une base front-end très solide, pensée pour le rendu moderne, le SEO et les performances. Firebase, lui, apporte des services backend puissants, managés et rapides à intégrer. Ensemble, ils permettent de lancer des produits sérieux avec une équipe réduite, ou même seul, sans tomber immédiatement dans la complexité d’une architecture trop lourde.

La vraie réussite, toutefois, ne vient pas seulement de la technologie. Elle vient de la manière dont on l’utilise. Une bonne séparation client/serveur, des règles de sécurité strictes, une structure de projet propre, une gestion claire des erreurs, et un soin particulier porté à l’expérience utilisateur font toute la différence. Avec ces bases, Firebase et Next.js deviennent bien plus qu’un simple raccourci de développement : ils deviennent un cadre de travail rassurant, productif et durable.

#Firebase avec Next.js #Next.js Firebase #Firebase Auth Next.js #Firestore Next.js #Firebase Storage Next.js #intégration Firebase Next.js #tutoriel Next.js Firebase #App Router Firebase #développement web moderne #authentification Firebase

Abonnez-vous à notre newsletter

12k+

Abonnés

Hebdomadaire

Fréquence

Gratuit

Toujours