Vous êtes sur la page 1sur 27

TP création d'un CRUD MongoDB

But : pouvoir expérimenter à partir d'un clone de projet à partir de github.

Création d'un mini blog en liaison avec une bdd MongoDB.

Video youtube : GTC Coding

Source code: https://github.com/Godsont/CRUD_MongoDB

Préparation projet
Créer un dossier projet et entrez dedans

mkdir tuto_crud_mongo

cd tuto_crud_mongo

npx create-next-app@latest .

N'oubliez pas le . pour créer le projet dans le dossier courant.

Répondez aux questions :

Initializing project with template: app-tw


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- autoprefixer
- postcss
- tailwindcss
- eslint
- eslint-config-next

Installez ensuite mongoose , un ORM pour interagir avec mongodb. et react-icons pour des
icones.

npm i mongoose react-icons

O l d i d V C d
On ouvre le dossier dans Vs-Code
Dans le terminal on tapes :

npm run dev

Dans le navigateur on a :

Effacer les styles


Dans app/glbals.css

Ne laissez que :

@tailwind base;
@tailwind components;
@tailwind utilities;

Modification de la page.js
Dans app , renommez page.js en page.jsx

export default function Home() {


return (

<h2>Coucou</h2>

);
}

Création des composant


A la racide du projet, créez un dossier components
Dans ce dossier, créez trois composants vide :

Composant Navbar.jsx
import Link from "next/link"

export default function Navbar() {


return (
<nav className="flex justify-between items-center bg-slate-800 px-8 py-3">
<Link className="text-white font-bold" href={"/"}>TPII Coding</Link>
<Link className="bg-white p-2" href={"/addTopic"}>Ajouter un article</Link>
</nav>
)
}

Composant RemoveBtn.jsx
import { HiOutlineTrash } from "react-icons/hi"

export default function RemoveBtn(){


return <button className="text-red-400" >
<HiOutlineTrash size={24}/>
</button>
}

Composant TopicsList.jsx
import RemoveBtn from "@/components/RemoveBtn";
import Link from "next/link";
import { HiPencilAlt } from "react-icons/hi";

export default function TopicsList() {


return (
<>
<div className="p-4 border border-slate-300 my-3 flex justify-between gap-5
items-start">
<div>
<h2 className="font-bold text-2xl">Titre Article</h2>
<div>{"Description de l'article"}</div>
</div>
<div className="flex gap-2">
<RemoveBtn/>
<Link href={'/editTopic/123'}>
<HiPencilAlt size={24} />
</Link>
</div>
</div>
</>
);
}

Modification du layout
Toutes les pages vont partager le layout.jsx , on y placera donc le composant navbar

import Navbar from "@/components/Navbar";


...//
return (
<html lang="en">
<body className={inter.className}>
<div className="max-w-3xl mx-auto p-4">
<Navbar />
<div className="mt-8">
{children}
</div>
</div>
</body>
</html>
);
...//

Création des routes


Créer des dossier dans app qui portent le même nom que les liens dans les composant <Link/>

ex : pour la route pour <Link href={'/addTopic'}> dans Navbar.jsx

export default function AddTopic() {


return (
<form className="flex flex-col gap-3">
<input
className="border border-slate-500 px-8 py-2"
type="text"
placeholder="Titre de l'article"
/>
<input
className="border border-slate-500 px-8 py-2"
type="text"
placeholder="Description de l'article"
/>
<button className="bg-blue-400 font-bold text-white py-3 px-6 w-
fit">Ajouter article</button>
</form>
);
}

Création des routes dynamiques


Créez un dossier nommé [id] et créez une page.jsx dans celui-ci.

Dans editTopic/[id]/page.jsx

export default function EditTopic() {


return <div>Edit Articles</div>
}

Testez

On a :
Création du composant EditTopicForm
Dans components\EditTopicForm/page.jsx
Copiez le formulaire à partir de app\addTopic\page.jsx

export default function EditTopicForm() {


return (
<form className="flex flex-col gap-3">
<input
className="border border-slate-500 px-8 py-2"
type="text"
placeholder="Titre de l'article"
/>
<input
className="border border-slate-500 px-8 py-2"
type="text"
placeholder="Description de l'article"
/>
<button className="bg-blue-400 font-bold text-white py-3 px-6 w-fit">
{"Modifier l'article"}
</button>
</form>
);
}

Dans app\editTopic\page.tsx

import EditTopicForm from "@/components/EditTopicForm";

export default function EditTopic() {


return <EditTopicForm/>
}

BACK END
Installation MongoDB (en version cloud)
MongoDB peut être installé sur votre machine local ou être utilisé en version cloud database.

Nous allons créer une base de donnée MongoDB à partir de leur cloud. (https://www.mongodb.co
m/atlas)

Le service est gratuit et ne nécessite pas de carte de crédit.

Pour avoir accès au cloud MongoDB et ensuite pouvoir créer votre propre base de données,
rendez-vous au https://www.mongodb.com/atlas et cliqué sur le bouton "Try Free".

Choisir le service Atlas, remplir le questionnaire et cliquer "Get started free"


Une fois votre compte créé, rendez-vous sur le panneau de configuration, vous devriez voir
quelque chose comme cela.

Cliquez sur "Build a Database", ensuite choisir l'option "Free Shared" et cliquer sur "Create".
Ensuite, conserver les options par défaut sauf la dernière option qui est "Cluster Name" et
changer le nom pour "NodeExpress". Enfin cliquer sur "Create Cluster"

Créer un nouveau projet


Votre compte MongoDB est créer et activé nous allons maintenant créer nun nouveau projet

Nommer votre projet : (ici, crud)


Cliquer ensuite sur Next , laissez tout par défaut) et cliquer sur Create Project
Créez un nom d'utilisateur et un mot de passe et surtout :
Notez-les !!

Mot de passe

Username : trentin
NqGpQKTbtcMdWtsy
Ensuite tapez dans IP Adresss la valeur 0.0.0.0/0 ce qui permet de se connecter à votre
base de données de n'importe où (pour le tuto).

Cliquez sur Finishand Close

Cliquez sur Go to Overview puis sur Connect


Choisissez MongoDB for VS Code

Copiez le lien pour VS Code


Dans visual Studio

Créez à la racine du projet un fichier .env (sans extension )

Les fichiers .env permettent de stocker des configurations spécifiques au projet, telles que
des identifiants de base de données, des clés API, et d'autres secrets, de manière sécurisée
et centralisée.

Dans le fichier .env

Collez l'uri copié dans MongoDB for VS Code

Ajoutgez le nom de votre base de donnée à la fin .../crud_db

MONGODB_URI=mongodb+srv://trentin:<password>@cluster0.r5dczs2.mongodb.net/crud_db

Remplacer <password> par le mot de passe que vous avez noté précédemment.

Prenez l'habitude de relancer vos serveur à chaque changement dans un fichier de


configuration.

Relancez dans le terminal : Ctrl+C et `npm run dev

vous pouvez ajouter .env dans votre .gitignore pour éviter d'exposer vos variables
d'environnement.

Librairie mongo et modéle


Dans le projet créez un dossier libs

A l'intérieur, créez un fichier mongodb.js


import mongoose from "mongoose"

const connectMongoDB = () => {


try {
mongoose.connect(process.env.MONGODB_URI)
console.log("MongoDB connéctée")

} catch (error) {
console.log(error)
}
}

export default connectMongoDB()

Modéles

Pour les modèles Créez un dossier models

Dans l'architecture Modèle-Vue-Contrôleur (MVC), le "modèle" représente la partie de


l'application qui gère les données et la logique métier. C'est le composant central qui
interagit directement avec la base de données ou toute autre source de données, traite
ces données (opérations CRUD et envoie les résultats à la vue, qui sera ensuite présentée à
l'utilisateur.

Créez un fichiers topic.js

// topic.js
import mongoose, { Schema } from "mongoose";

const topicSchema = new Schema(


{
title: String,
description: String,
},
{
timestamps: true,
}
);

const Topic = mongoose.models.Toppic || mongoose.model("Toppic", topicSchema);

export default Topic;


Explications

Ce code utilise Mongoose, une bibliothèque ODM (Object Data Modeling) pour MongoDB et
Node.js, pour définir un modèle de données pour une collection MongoDB appelée "Topic"

1. Importation de Mongoose et du Schéma : La première ligne importe Mongoose et son


constructeur Schema . Mongoose est utilisé pour faciliter la gestion des interactions avec la
base de données MongoDB[1].

2. Définition du Schéma : topicSchema est défini comme une nouvelle instance de Schema .
Ce schéma décrit la structure des documents dans la collection "Topic" avec deux champs :
title et description , tous deux de type String . L'option timestamps: true ajoute
automatiquement deux champs : createdAt et updatedAt , qui enregistrent la date de
création et la dernière modification du document[4].

3. Création du Modèle : La variable Topic est déclarée pour stocker le modèle.


mongoose.models.Topic vérifie si le modèle "Topic" existe déjà (pour éviter la re-
déclaration lors de hot reloading (*) en développement). Si le modèle n'existe pas,
mongoose.model("Topic", topicSchema) crée et retourne un nouveau modèle basé sur
topicSchema . Notez l'erreur dans la chaîne "Toppic" qui devrait être "Topic", et le code tente
d'utiliser un modèle potentiellement inexistant à cause de cette faute de frappe.

4. Exportation du Modèle : Finalement, Topic est exporté, permettant à d'autres parties de


l'application d'interagir avec la collection "Toppic" dans MongoDB en utilisant ce modèle pour
créer, lire, mettre à jour et supprimer des documents conformes au schéma défini[3].

(*) (hot reloading) ou rechargement dynamique se réfère à la capacité d'un système à


mettre à jour des composants ou des logiciels sans devoir redémarrer ou interrompre le
système en cours d'exécution.

Création de l'API
Pour créer les routes des api, on va utiliser le mode de programmation asynchrone avec
async et await pour gérer les opérations de base de données de manière efficace et non
bloquante.

On utilisera aussi Postman, pour tester les API

Nous allons modifier d'abord le fichier mongodb.js en ajpoutant async et await

import mongoose from "mongoose"

const connectMongoDB = async () => {


try {
await mongoose.connect(process.env.MONGODB_URI)
console.log("MongoDB connéctée")

} catch (error) {
console.log(error)
}
}

export default connectMongoDB()


Dans le dossier app , créez un dossier api . dans ceui-ci un sous dossier topics et dans ce
dossier un fichiers route.js

// app > api > topics > route.js


import connectMongoDB from "@/libs/mongodb";

import Topic from "@/models/topic";

import { NextResponse } from "next/server";

export async function POST(request) {


const { title, description } = await request.json();
await connectMongoDB();
await Topic.create({ title, description });

return NextResponse.json({ message: "Article créé" }, { status: 201 });


}

Explications

1. Importations :

`NextResponse est importé de "next/server" pour faciliter la création de réponses HTTP


dans les fonctions API.

2. Fonction POST :

Cette fonction asynchrone prend une requête HTTP request , extrait les données JSON
( title , description ), se connecte à MongoDB, crée un nouveau document Topic
avec ces données, et retourne une réponse JSON indiquant que l'article a été créé, avec
un code de statut HTTP 201 (Créé).

Testons avec POSTMAN

Ouvrir POSTMan et créez une requête POST sur l'URI http://localhost:3000/api/topics


Dans le Body de la requête, selectioinnez raw JSON et entrez le titre et la description

Envoyer la requête (Send) et vérifier le message de retour

Vérifiez dans Atlas si la collection a bien été crée.


Le document a bien été créé !

Avec Postman, créez deux documents supplémentaires.

{
"title":"CSS",
"description": "A cascading style sheet for websites"
}

Et

{
"title":"React",
"description": "A front End Framework for modern websites"
}
-Dans Atlas, rafraichissez la page.

Création de la méthodeGet dans api/topics/


Modifiez le code de toute.js et ajoutez la fonction GET()

// app>api>topics> route.js

// ...... fin du fichier

export async function GET() {


await connectMongoDB();
const topics = await Topic.find();
return NextResponse.json({ topics });
}

Explications

Renvoi des données : La dernière ligne, return NextResponse.json({ topics }); , utilise
NextResponse (une API de Next.js pour manipuler les réponses HTTP) pour envoyer les données
récupérées ( topics ) au client au format JSON. Cela permet au frontend de l'application d'accéder
et d'afficher ces données[1].

Testez dans Postman.


{
"topics": [
{
"_id": "65ccc4df67650cc123566181",
"title": "HTML",
"description": "A marckup language for websites",
"createdAt": "2024-02-14T13:49:19.638Z",
"updatedAt": "2024-02-14T13:49:19.638Z",
"__v": 0
},
// ......
]
}

Création de la méthode Delete dans api/topics/


// app>api>topics> route.js

// ...... fin du fichier

export async function DELETE(request) {


const id = request.nextUrl.searchParams.get("id");
await connectMongoDB();
await Topic.findByIdAndDelete(id);
return NextResponse.json({ message: "Article supprimé" }, { status: 200 });
}

Testez dans Postman.

Sélectionnez l'id du document à supprimer.

Entrez les paramètres (ou ecrivez les directement dans l'URI)

Envoyez et vérifiez la suppression du document


Création de la méthode Update dans api/topics/
La méthode update est dynamique (on doit indiquer l' id du document à modifier, aussi nous
allons créez une route dynamique.

Nous créons donc un dossier [id] dans app/api/topics

Créez un fichier route.js

// app>api>topics> [id] > route.js

import connectMongoDB from "@/libs/mongodb";


import Topic from "@/models/topic";
import { NextResponse } from "next/server";

export async function PUT(request, { params }) {


const { id } = params;
const { newTitle: title, newDescription: description } = await request.json();
await connectMongoDB();
await Topic.findByIdAndUpdate(id, { title, description });
return NextResponse.json({ message: "Article modifié" }, { status: 200 });
}

Explications
La fonction PUT :

export async function PUT(request, { params }) { ... }

: On définit ici une fonction asynchrone

PUT

(qui correspond à la méthode HTTP PUT utilisée pour la mise à jour de ressources sur le
serveur) et on l'exporte pour qu'elle puisse être utilisée ailleurs dans l'application. Elle prend
deux paramètres :

request : L'objet de la requête HTTP entrante, contenant les données envoyées par le
client.
{ params } : Un objet déstructuré (*) directement extrait de l'argument de contexte,
contenant les paramètres de route, comme l' id de l'article à mettre à jour.

Extraction et mise à jour des données :

const { id } = params; : On extrait l' id de l'article à mettre à jour depuis les paramètres
de la requête.

const { newTitle: title, newDescription: description } = await request.json(); :


On attend la résolution de la promesse request.json() pour extraire et renommer
newTitle en title et newDescription en description depuis le corps de la requête.

await Topic.findByIdAndUpdate(id, { title, description }); : Cette ligne utilise la


méthode findByIdAndUpdate du modèle Topic pour trouver un document par son id et
mettre à jour son titre et sa description avec les nouvelles valeurs.

(*) La déstructuration d'objet dans JavaScript est une syntaxe expressive et concise pour
extraire des valeurs de propriétés d'un objet. Dans la ligne de code const { id } =
params; , la déstructuration est utilisée pour extraire la valeur de la propriété id de l'objet
params .

Voici comment cela fonctionne :

params est un objet qui contient des paires clé-valeur. Par exemple, il pourrait
ressembler à { id: '123', autrePropriete: 'valeur' } .

const { id } = params; crée une nouvelle variable nommée id et y assigne la


valeur de la propriété id de l'objet params .

Cette syntaxe rend le code plus lisible et moins verbeux, car elle évite d'avoir à accéder à la
propriété de l'objet de manière explicite (par exemple, const id = params.id; ). Elle est
particulièrement utile pour extraire plusieurs propriétés d'un objet en une seule instruction.

Testez avec Postman

Vérifiez le résultat avec GET ou dans Atlas


Création de la méthode GET/id api/topics/
Pour retourner un document particulier (en fonction de son id)

// app>api>topics> [id] > route.js

//.... fin fichier

export async function GET(request, { params }) {


const { id } = params;
await connectMongoDB();
const topic = await Topic.findOne({ _id: id });
return NextResponse.json({ topic }, { status: 200 });
}

Toutes ls opérations CRUD sont maintenant fonctionnelles.

Connexion du backend au frontend


Modification du composant TopicsList.
On va intégrer en lieu et place des données en dur (ex : Titre Article ) par les données dynamiques

On va créer une fonction pour récupérer les datas

// components > TopicsList > TopicsList.jsx

//....

const getTopics = async () => {


try {
const res = await fetch("http://localhost:3000/api/topics", {
cache: "no-store",
});

if (!res.ok) {
throw new Error("Fetch a échoué");
}
return res.json();
} catch (error) {
console.log("Erreur de chargement d'article: ", error);
}
};
SI vous avez des messages d'erreurs concernant la présence obligatoire de key dans le map ,
vous pouvez ajouter :

/* eslint-disable react/jsx-key */

On règlera le pb plus tard

Explications
Le deuxième argument passé à la fonction fetch() est un objet de configuration. Dans cet
exemple, il y a une seule propriété définie dans cet objet :

cache: "no-store" : cette propriété indique au navigateur de ne pas mettre en cache la


réponse de la requête. Cela signifie que chaque fois que cette requête est effectuée, le
navigateur enverra la demande au serveur, même si une copie précédente de la réponse est
disponible en cache.

Injection des datas

/* eslint-disable react/jsx-key */
import RemoveBtn from "@/components/RemoveBtn";
import Link from "next/link";
import { HiPencilAlt } from "react-icons/hi";

const getTopics = async () => {


try {
const res = await fetch("http://localhost:3000/api/topics", {
cache: "no-store",
});

if (!res.ok) {
throw new Error("Fetch a échoué");
}
return res.json();
} catch (error) {
console.log("Erreur de chargement d'article: ", error);
}
};

export default async function TopicsList() {


const {topics } = await getTopics()
return (
<>
{ topics.map((t) => (

<div className="p-4 border border-slate-300 my-3 flex justify-between


gap-5 items-start">
<div>
<h2 className="font-bold text-2xl">{t.title}</h2>
<div>{t.description}</div>
</div>
<div className="flex gap-2">
<RemoveBtn />
<Link href={`/editTopic/${t._id}`}>
<HiPencilAlt size={24} />
</Link>
</div>
</div>
))}
</>
);
}

export default async function TopicsList() {


const {topics } = await getTopics()
return (
<>
{ topics.map((t) => (

<div className="p-4 border border-slate-300 my-3 flex justify-between


gap-5 items-start">
<div>
<h2 className="font-bold text-2xl">{t.title}</h2>
<div>{t.description}</div>
</div>
<div className="flex gap-2">
<RemoveBtn />
<Link href={`/editTopic/${t._id}`}>
<HiPencilAlt size={24} />
</Link>
</div>
</div>
))}
</>
);
}

Déstructuration : const {topics } = await getTopics() utilise la déstructuration pour


extraire

topics de l'objet retourné par getTopics() .

Il est très courant d'utiliser une fonction anonyme avec la méthode map() pour générer un
fragment de code ou un composant pour chaque élément d'un tableau et les afficher en
utilisant React ou une autre bibliothèque front-end.
Vérification

Les données de la bdd sont injectées et mappées dans la vue.

Modification de AddTopic/page.jsx

Par default dans Next.js les composants sont des composants serveur

Next.js supporte plusieurs formes de rendu : côté serveur (SSR), statique (SSG), et côté client.
Voici pourquoi 'use client' peut être utilisé :

1. Rendu Client-Side : L'indication 'use client' dans un composant React spécifie que ce
composant doit être rendu exclusivement côté client. Cela signifie que le composant ne
sera pas pré-rendu sur le serveur lors de la génération de la page, mais sera plutôt
exécuté et rendu dans le navigateur de l'utilisateur. Cela peut être utile pour des
parties de l'application qui dépendent fortement des interactions utilisateur
(comme les formulaires) ou des données spécifiques au client qui ne sont pas
disponibles lors du rendu côté serveur.

2. Performance et Optimisation : Le rendu côté client peut améliorer les performances


et le temps de chargement initial de la page en réduisant la charge de travail sur le
serveur.

3. Séparation des préoccupations : Utiliser 'use client' aide à séparer clairement les
composants qui doivent être rendus côté serveur de ceux exclusivement destinés au
rendu côté client. Cela simplifie le développement et la maintenance en permettant aux
développeurs de concentrer leurs efforts d'optimisation sur les parties spécifiques de
l'application qui bénéficient le plus du rendu côté client.

// app > addTopic> page.jsw

"use client";

import { useRouter } from "next/navigation";


import { useState } from "react";

export default function AddTopic() {


const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const router = useRouter();

const handleSubmit = async (e) => {


e.preventDefault();

if (!title || !description) {
alert("Le title et la description sont requis.");
return;
}

try {
const res = await fetch("http://localhost:3000/api/topics", {
method: "POST",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify({ title, description }),
});

if (res.ok) {
router.push("/");
} else {
throw new Error("La créatoin d'article a échoué");
}
} catch (error) {
console.log(error);
}
};

return (
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
<input
onChange={(e) => setTitle(e.target.value)}
value={title}
className="border border-slate-500 px-8 py-2"
type="text"
placeholder="Titre Article"
/>

<input
onChange={(e) => setDescription(e.target.value)}
value={description}
className="border border-slate-500 px-8 py-2"
type="text"
placeholder="Description de l'Article"
/>

<button
type="submit"
className="bg-green-600 font-bold text-white py-3 px-6 w-fit"
>
Ajouter
</button>
</form>
);
}

Explications

La directive "use client" indique que ce code est destiné à s'exécuter sur le navigateur client et
non sur le serveur.

Deux états sont déclarés à l'aide du Hook useState : title pour stocker le titre et
description pour stocker la description. Les fonctions setTitle et setDescription
permettent de mettre à jour ces états respectifs.

Le router est obtenu en utilisant le Hook useRouter de Next.js.

La fonction handleSubmit est définie. Elle est appelée lors de la soumission du formulaire et
est définie comme une fonction asynchrone.

Dans handleSubmit, on empêche le comportement de soumission par défaut du formulaire à


l'aide de e.preventDefault().

Ensuite, on vérifie si title et description sont vides. Si c'est le cas, une alerte est affichée
demandant de remplir les champs requis.

Tester et vérifier l'insertion.

Vous aimerez peut-être aussi