Dans le cadre de la création d'une application avec Next.js, la structure des pages et la gestion des routes sont des éléments cruciaux. À travers l'App Router, Next.js permet de définir des routes dynamiques et de créer une navigation fluide tout en optimisant le rendu des pages. Ce chapitre explore les principes fondamentaux du routage dans Next.js, y compris l'organisation des dossiers, l'utilisation des composants de layout et des segments dynamiques.

Une fois que vous avez un fichier page.js dans le répertoire racine de l'application, qui correspond à l'URL /, vous pouvez étendre cette structure pour créer des pages supplémentaires telles que la page d'accueil, une page de liste des voitures, une page dédiée à l'affichage des détails d'une voiture spécifique, une page privée pour l'ajout de voitures (accessible uniquement aux utilisateurs autorisés), et une page de connexion.

Le premier pas pour construire la structure des pages est de créer un répertoire pour chaque route nécessaire. Par exemple, pour afficher une liste de voitures, créez un dossier cars dans le répertoire /app et un fichier page.js à l’intérieur de ce dossier. Ce fichier pourrait ressembler à ceci :

javascript
const Cars = () => { return ( <div> Cars </div> ); }; export default Cars;

Ensuite, pour afficher les détails d'une voiture en particulier, vous devez créer un sous-dossier dynamique dans /app/cars, en utilisant une syntaxe de crochets ([id]). Ce dossier contient un fichier page.js qui rendra un composant CarDetails :

javascript
const CarDetails = () => { return ( <div> Car Details </div> ); }; export default CarDetails;

Ce type de structure permet de gérer dynamiquement les paramètres d’URL, comme l’identifiant unique de chaque voiture. Cela simplifie la gestion des données et optimise l’architecture de l’application.

La gestion des pages privées et des pages de connexion suit un processus similaire. Vous pouvez créer un fichier page.js dans un dossier /app/private et un autre dans /app/login. L'organisation de ces pages assure une séparation claire des différentes parties de l’application, tout en permettant une gestion spécifique des utilisateurs, notamment pour les pages protégées par un système d'authentification.

Un autre aspect essentiel de Next.js est l'utilisation de layouts pour partager des composants d'interface utilisateur communs à plusieurs pages. Dans le cas de l'application de voitures, par exemple, vous pouvez créer un fichier layout.js dans le dossier /app/cars, qui contiendra un composant de mise en page de base, utilisé pour toutes les pages relatives aux voitures. Ce layout pourrait ressembler à ceci :

javascript
const Layout = ({ children }) => {
return ( <div> Cars Layout {children} </div> ); }; export default Layout;

Ce layout sera appliqué à toutes les pages enfants du dossier cars, garantissant que l’interface utilisateur reste cohérente entre les différentes routes, tout en préservant l'état et en évitant un nouveau rendu à chaque navigation.

Les layouts et les templates offrent de puissantes fonctionnalités supplémentaires, comme la possibilité d’ajouter des animations de page ou d’organiser des segments de routes spécifiques. Par exemple, les segments de routes avec des crochets [...] permettent de créer des routes qui captent plusieurs paramètres, et les groupes de routes permettent de modifier le comportement de l'URL sans affecter le rendu des pages.

Au-delà de la structure des pages et des routes, Next.js permet une distinction fondamentale entre les composants serveur et client. Les composants serveur sont rendus sur le serveur, ce qui les rend idéaux pour la gestion des données sensibles ou des requêtes API, tandis que les composants client peuvent interagir avec le navigateur et utiliser des fonctionnalités telles que les hooks React ou les API du navigateur, comme le stockage local ou la géolocalisation.

L’ajout de la directive "use client" en haut d’un fichier permet de convertir un composant serveur en un composant client. Cela crée une séparation claire entre la logique de rendu côté serveur et côté client, en fonction des besoins de votre application. Pour créer un composant de navigation simple, par exemple, vous pouvez créer un fichier NavBar.js et utiliser le composant Link de Next.js pour gérer la navigation entre les pages :

javascript
import Link from "next/link"; const Navbar = async () => { return ( <div> <nav>
<Link href="/">Home</Link>
<Link href="/cars">Cars</Link>
<Link href="/private">Private</Link>
<Link href="/login">Login</Link> </nav> </div> ); }; export default Navbar;

L’utilisation de ce composant de navigation dans l'application permet de passer facilement d'une page à une autre tout en bénéficiant de l’optimisation du préchargement des données par Next.js.

À travers l'intégration de ces concepts, vous pouvez créer une application bien structurée, modulaire et optimisée pour les performances. Cependant, il est essentiel de toujours garder à l’esprit que la gestion des routes et des layouts dans Next.js permet non seulement de contrôler la structure et la navigation, mais aussi d'améliorer l'expérience utilisateur et de simplifier l'intégration des fonctionnalités de votre application.

L'un des points cruciaux à comprendre est l’importance de choisir judicieusement entre les composants serveur et client en fonction des besoins spécifiques de chaque page ou fonctionnalité. Le bon usage des layouts, templates, et segments dynamiques permet de créer des applications robustes et scalables tout en optimisant l’interaction avec les utilisateurs. La clé réside dans la capacité à bien structurer les dossiers, gérer les données de manière efficace, et comprendre les différences entre la logique serveur et client dans un environnement Next.js.

Comment utiliser les composants serveur de Next.js pour charger des données sans utiliser de hooks

Dans le cadre du développement avec Next.js, l'une des fonctionnalités les plus puissantes et innovantes réside dans l'utilisation des composants serveur pour charger des données, sans recourir aux méthodes traditionnelles telles que les hooks et les états. Ce paradigme permet de simplifier considérablement l'architecture des applications web tout en optimisant leur performance.

Prenons un exemple pratique pour illustrer cela. Imaginons un projet où vous devez afficher des informations sur des voitures provenant d'un serveur backend, comme FastAPI. Au lieu de charger les données via des hooks useState ou useEffect dans le côté client, Next.js propose une approche basée sur des composants serveur, qui permet de charger les données directement depuis le serveur avant même de rendre la page dans le navigateur.

Configuration initiale

Pour commencer, il est nécessaire de configurer l'URL de l'API dans un fichier .env placé à la racine du projet Next.js. Ce fichier pourrait contenir une ligne telle que :

ini
API_URL=http://127.0.0.1:8000

Cette URL sera utilisée pour effectuer des appels API à partir des composants serveur. Une fois ce fichier créé et l'API URL définie, il suffit d'accéder à cette variable d'environnement dans le code via process.env.API_URL.

Notez que lorsqu'il s'agit de récupérer des données côté serveur, il est parfaitement acceptable de ne pas exposer l'URL de l'API dans le navigateur. Cependant, si vous deviez effectuer des appels API côté client, vous devrez préfixer cette variable avec NEXT_PUBLIC_ pour la rendre accessible au client.

Chargement des données dans un composant serveur

Supposons que vous souhaitiez afficher toutes les voitures présentes dans votre base de données. Vous pouvez créer un fichier page.js sous le répertoire /app/cars/, où le chargement des données se fait de manière asynchrone :

javascript
import Link from "next/link"; const Cars = async () => {
const data = await fetch(`${process.env.API_URL}/cars/`, {
next: { revalidate: 10 } }); const cars = await data.json(); return ( <> <h1>Cars</h1> {cars.map((car) => ( <div key={car.id}> <h2>{car.brand} {car.make} from {car.year}</h2> </div> ))} </> ); } export default Cars;

Dans cet exemple, vous utilisez la fonction fetch de Next.js, qui est une extension du fetch natif du Web API. Ce qui distingue cette version, c'est l'option next, qui permet de gérer des fonctionnalités supplémentaires comme la revalidation des données. Ici, les données sont revalidées toutes les 10 secondes pour s'assurer que le contenu affiché est toujours à jour, ce qui est particulièrement utile dans des environnements où les données sont fréquemment modifiées.

La gestion des erreurs

Un autre aspect essentiel dans le développement avec Next.js est la gestion des erreurs. Il est impératif de capturer et de gérer les erreurs qui peuvent survenir lors de la récupération des données. Next.js permet de créer un fichier spécifique error.js pour gérer les erreurs au sein d'un groupe de routes.

Voici un exemple simple de gestion d'erreur dans le fichier error.js :

javascript
"use client";
const error = () => { return ( <div> <p>Il y a eu une erreur lors du chargement des données des voitures !</p> </div> ); } export default error;

Ce fichier capte les erreurs qui se produisent au niveau de ce composant spécifique et affiche un message d'erreur à l'utilisateur. Il est possible de tester cette gestion d'erreur en générant une erreur intentionnelle dans le composant qui récupère les données, ce qui permettra de déclencher l'affichage de ce message d'erreur.

Rendu statique et gestion des images

Next.js ne se limite pas à la récupération des données serveur. Le framework permet également de générer des pages statiques lors de la phase de build, offrant ainsi des performances maximales, semblables à celles d'un générateur de sites statiques comme Gatsby.js. Le rendu statique est particulièrement adapté aux pages qui ne nécessitent pas de données dynamiques basées sur l'utilisateur, comme des articles de blog ou des pages de documentation.

Un autre outil puissant de Next.js est son composant Image, qui permet d'optimiser les images chargées depuis des domaines externes comme Cloudinary. Voici un exemple d'intégration de ce composant dans une page de détails d'une voiture :

javascript
import Image from "next/image";
const CarDetails = async ({ params }) => { const carId = params.id;
const res = await fetch(`${process.env.API_URL}/cars/${carId}`, {
next: { revalidate: 10 } }); if (!res.ok) { redirect("/error"); } const data = await res.json(); return ( <div> <h1>{data.brand} {data.make}</h1> <Image src={data.imageUrl} alt={`${data.brand} ${data.make}`} width={500} height={300} /> </div> ); } export default CarDetails;

Dans ce code, le composant Image est utilisé pour afficher l'image d'une voiture à partir d'une URL externe. Avant cela, il est nécessaire de configurer Next.js pour autoriser le chargement d'images depuis des sources externes en modifiant le fichier next.config.js comme suit :

javascript
const nextConfig = { images: { remotePatterns: [ { hostname: 'res.cloudinary.com', }, ], }, }; export default nextConfig;

Ce qu'il faut retenir

Le véritable atout de Next.js réside dans sa capacité à allier les performances du rendu statique et la flexibilité du rendu dynamique côté serveur. Les composants serveur permettent de charger les données directement avant le rendu de la page, offrant ainsi une expérience utilisateur rapide et fluide. De plus, la gestion d'erreurs et l'intégration avec des services tiers pour le stockage d'images simplifient encore davantage le processus de développement.

En revanche, il est essentiel de bien comprendre que certaines pages, en particulier celles contenant des données sensibles ou personnalisées, ne doivent pas être statiquement générées. L'utilisation de la revalidation est un autre concept clé pour garantir que les données affichées sont toujours à jour. Les erreurs doivent être anticipées et gérées de manière proactive pour garantir une expérience utilisateur optimale.

Comment créer des pages protégées et gérer l'authentification avec Next.js et Iron Session

L'authentification est un aspect essentiel de toute application web moderne. Dans le cadre de cette discussion, nous allons examiner un processus simple mais efficace pour implémenter l'authentification dans une application Next.js à l'aide du package Iron Session. Cette approche garantit que seules les personnes authentifiées peuvent accéder à certaines pages ou fonctionnalités, tout en offrant une gestion fluide des sessions utilisateur.

Dans un premier temps, la création d'un système de connexion repose sur l’utilisation de Server Actions en Next.js. Un utilisateur doit entrer ses identifiants dans un formulaire de connexion, et si ceux-ci sont valides, une session est établie. Cette session contient un jeton d’authentification (JWT) ainsi qu’un nom d'utilisateur. Si les informations de connexion sont incorrectes, la session est détruite et l'utilisateur est redirigé vers la page de connexion.

Le code suivant montre un exemple d'intégration d'un formulaire de connexion dans un composant React :

javascript
"use client" import { login } from "@/actions" import { useFormState } from "react-dom"; const LoginForm = () => { const [state, formAction] = useFormState(login, {}) return ( <form onSubmit={formAction}>
<label htmlFor="username">Username</label>
<input type="text" id="username" name="username" required /> <label htmlFor="password">Password</label> <input type="password" id="password" name="password" required />
<button type="submit">Sign In</button>
{JSON.stringify(state, null, 2)}
</form> ) } export default LoginForm

Ce formulaire utilise le hook useFormState, qui est un ajout récent à l’écosystème React (il provient du package react-dom). Ce hook permet de suivre l’état du formulaire et de gérer les erreurs qui peuvent être renvoyées par le serveur.

Une fois ce formulaire intégré à la page de connexion, l'utilisateur peut entrer ses identifiants. Si ceux-ci sont valides, il sera redirigé vers une page privée où des actions protégées peuvent être réalisées, comme l'ajout de nouvelles voitures dans une base de données MongoDB. Pour protéger cette page, nous utilisons Iron Session pour vérifier si l'utilisateur est authentifié.

Voici comment protéger une page avec Iron Session :

javascript
import { getSession } from "@/actions"
import { redirect } from "next/navigation" const page = async () => { const session = await getSession() if (!session?.jwt) { redirect("/login") } return ( <div> <h1>Private Page</h1> {JSON.stringify(session, null, 2)} </div> ) } export default page

Dans ce code, la session de l'utilisateur est vérifiée. Si elle est invalide ou absente, l'utilisateur est redirigé vers la page de connexion. Si elle est valide, la page privée est rendue avec les données de la session.

En parallèle, il est crucial de pouvoir gérer la déconnexion des utilisateurs. Pour cela, nous ajoutons une fonction de déconnexion dans le fichier d'actions, qui permet de détruire la session et de rediriger l’utilisateur vers la page d’accueil :

javascript
export const logout = async () => { const session = await getSession() session.destroy() redirect("/") }

Ensuite, un bouton de déconnexion peut être intégré à la barre de navigation pour que l'utilisateur puisse se déconnecter à tout moment. Le composant de la barre de navigation vérifie si l'utilisateur est authentifié et affiche un bouton de connexion ou de déconnexion en conséquence :

javascript
import Link from "next/link" import { getSession } from "@/actions";
import LogoutForm from "./LogoutForm";
const Navbar = async () => { const session = await getSession() return ( <nav> <Link href="/">Farm Cars</Link>
<Link href="/cars">Cars</Link>
{session?.jwt ? (
<> <Link href="/private">Private</Link> <LogoutForm /> </> ) : (
<Link href="/login">Login</Link>
)} </nav> ) }
export default Navbar

Ce code conditionne l'affichage du lien de connexion ou de déconnexion en fonction de l'état de la session de l'utilisateur.

Dans cet environnement, l’application utilise donc Iron Session pour gérer l'authentification et la persistance de la session à travers plusieurs pages. En utilisant cette approche, vous pouvez facilement protéger n'importe quelle page de votre application en vérifiant la validité de la session avant de rendre l’accès à une page privée.

Il est aussi important de noter que, bien que Iron Session simplifie le processus de gestion de l'authentification, il reste essentiel de sécuriser la session côté serveur et de s'assurer que les données sensibles, comme le JWT, soient bien protégées pendant leur transmission. L'utilisation de cookies sécurisés (via HTTPS) et d'une expiration appropriée des tokens sont des pratiques recommandées pour renforcer la sécurité.

Comment implémenter un formulaire d'ajout de voiture dans une application avec Next.js et FastAPI ?

Dans cette section, nous allons aborder l'implémentation d'un formulaire permettant aux utilisateurs authentifiés d'ajouter de nouvelles voitures à l'application. Cette fonctionnalité est réalisée en utilisant les Actions Serveur de Next.js et l'API FastAPI pour effectuer une requête POST afin d'ajouter un véhicule à la base de données.

Lors de la création de ce formulaire, il est essentiel de prendre en compte plusieurs facteurs. Premièrement, la durée du cookie de session doit être définie de manière cohérente avec le JWT fourni par le backend de FastAPI. La propriété maxAge du cookie, définie dans le fichier /src/lib.js, joue un rôle crucial dans cette gestion, car elle garantit que l'authentification reste valide pendant la période définie par le token.

L'application, délibérément, ne dispose pas de fonctionnalité d'enregistrement des utilisateurs. L'idée est de limiter l'accès à un nombre restreint d'employés ou d'utilisateurs créés directement via l'API. Cependant, il est possible, dans un exercice pratique, de concevoir une page d'inscription des utilisateurs en utilisant le point de terminaison /users/register de l'API FastAPI.

Pour ajouter une nouvelle voiture, l'une des étapes initiales consiste à définir un composant InputField dans le fichier /src/components/InputField.js. Ce composant gère l'affichage de chaque champ de formulaire nécessaire pour l'ajout d'un véhicule, comme la marque, le modèle, l'année, le prix, etc. L'objectif est de rendre chaque champ réutilisable et facilement modifiable. Le composant InputField peut ressembler à ceci :

javascript
const InputField = ({ props }) => {
const { name, type } = props; return ( <div> <label htmlFor={name}>{name}</label> <input type={type} id={name} name={name} /> </div> ); };

Une fois ce composant créé, nous pouvons passer à la création du formulaire principal d'ajout de voiture dans le fichier /src/components/CarForm.js. Ce formulaire utilise l'hook useFormState, tout comme pour la gestion de l'authentification des utilisateurs. Un tableau de champs nécessaires pour l'ajout de voitures est défini, où chaque élément contient un nom de champ et un type associé :

javascript
const CarForm = () => { let formArray = [ { name: "brand", type: "text" }, { name: "make", type: "text" }, { name: "year", type: "number" }, { name: "price", type: "number" }, { name: "km", type: "number" }, { name: "cm3", type: "number" }, { name: "picture", type: "file" } ]; };

L'implémentation du formulaire consiste ensuite à mapper sur ce tableau de champs pour générer dynamiquement les éléments du formulaire. L'action createCar sera utilisée pour gérer l'envoi des données, et un bouton "Save new car" permet de soumettre les informations.

Le formulaire doit être affiché sur une page privée réservée aux utilisateurs authentifiés. Pour cela, il convient d'ajouter un contrôle d'authentification dans le fichier /src/app/private/page.js. Si l'utilisateur n'est pas authentifié (c'est-à-dire si son JWT n'est pas valide), l'utilisateur sera redirigé vers la page de connexion.

Voici un exemple de ce contrôle d'accès :

javascript
const page = async () => {
const session = await getSession(); if (!session?.jwt) { redirect("/login"); } return ( <div> <h1>Private Page</h1> <CarForm /> </div> ); };

Enfin, l'action createCar, qui s'occupe de la requête POST pour ajouter une voiture, est définie dans le fichier /src/actions.js. Elle utilise le JWT de l'utilisateur pour authentifier la requête. Si l'ajout de la voiture est réussi, l'utilisateur sera redirigé vers la page d'accueil.

javascript
export const createCar = async (state, formData) => {
const session = await getSession(); const jwt = session.jwt; const result = await fetch(`${process.env.API_URL}/cars/`, { method: "POST", headers: { Authorization: `Bearer ${jwt}`, }, body: formData, }); const data = await result.json(); if (result.ok) { redirect("/"); } else { return { error: data.detail }; } };

L'implémentation de cette fonctionnalité d'ajout de voitures permet à l'application d'être interactive et dynamique, tout en garantissant un haut niveau de sécurité grâce à l'authentification basée sur JWT. Les utilisateurs authentifiés peuvent ainsi insérer des voitures et visualiser les informations sur la page principale après un délai de revalidation de 15-20 secondes.

À ce stade, l'application est fonctionnelle, mais il est important de noter que l'optimisation du SEO et la configuration de métadonnées peuvent également améliorer la visibilité de l'application. L'intégration de balises <title> et <meta> est essentielle pour le référencement et pour offrir aux utilisateurs une meilleure expérience.