Contactez-nous

Implémentation avec Express.js et MongoDB

Codons notre API RESTful ! Mise en place du serveur Express, connexion à MongoDB avec Mongoose, définition du modèle et implémentation des routes CRUD pour les tâches.

Du plan à la réalité : construire le serveur et la persistance

Avec la conception de notre API de gestion de tâches bien définie (modèle de données et endpoints), nous pouvons maintenant passer à l'implémentation concrète. Cette étape consiste à traduire notre plan en code fonctionnel en utilisant les outils que nous avons choisis : Express.js pour la structure du serveur et la gestion des routes, et MongoDB associé à Mongoose pour la persistance des données.

Nous allons commencer par initialiser notre projet Node.js, installer les dépendances nécessaires, et mettre en place la structure de base de notre serveur Express. Ensuite, nous établirons la connexion à notre base de données MongoDB. Une fois la connexion établie, nous implémenterons le modèle `Task` en utilisant un schéma Mongoose, en respectant la structure définie lors de la conception. Enfin, nous créerons les routes Express correspondant aux endpoints CRUD et écrirons la logique (dans des contrôleurs ou directement dans les gestionnaires de routes) pour interagir avec la base de données via notre modèle Mongoose.

Initialisation du projet et dépendances

Commençons par créer un nouveau répertoire pour notre projet et initialisons-le avec npm (ou yarn) :

mkdir task-api
cd task-api
npm init -y

Installons ensuite les dépendances essentielles :

npm install express mongoose dotenv
  • `express` : Notre framework web.
  • `mongoose` : Notre ODM pour MongoDB.
  • `dotenv` : Pour charger les variables d'environnement depuis un fichier `.env` (très utile pour la configuration).

Installons également `nodemon` comme dépendance de développement pour redémarrer automatiquement le serveur lors des modifications du code :

npm install --save-dev nodemon

Ajoutons un script `dev` dans notre `package.json` pour lancer le serveur avec nodemon :

// package.json (section scripts)
"scripts": {
  "start": "node src/server.js", // Commande pour la production
  "dev": "nodemon src/server.js" // Commande pour le développement
}

Créez un fichier `.env` à la racine pour nos variables d'environnement (n'oubliez pas d'ajouter `.env` à votre fichier `.gitignore` !) :

# .env
PORT=3000
# Remplacez par votre URL de connexion MongoDB (locale ou Atlas)
MONGODB_URL=mongodb://127.0.0.1:27017/task-api-db

Créez une structure de projet de base, par exemple :

mkdir src
mkdir src/models
mkdir src/routes
touch src/server.js
touch src/db/mongoose.js
touch src/models/task.js
touch src/routes/taskRoutes.js

Connexion à MongoDB avec Mongoose

Créons le fichier de connexion à la base de données (`src/db/mongoose.js`) :

// src/db/mongoose.js
const mongoose = require('mongoose');
require('dotenv').config(); // Charger les variables de .env

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URL, {
      useNewUrlParser: true, // Options pour éviter les warnings de dépréciation
      useUnifiedTopology: true,
      // useCreateIndex: true, // Non nécessaire dans Mongoose 6+
      // useFindAndModify: false // Non nécessaire dans Mongoose 6+
    });
    console.log('Connexion à MongoDB établie avec succès.');
  } catch (error) {
    console.error('Erreur de connexion à MongoDB:', error.message);
    process.exit(1); // Arrêter l'application si la connexion échoue
  }
};

module.exports = connectDB;

Définition du modèle de tâche (Mongoose Schema)

Implémentons notre modèle `Task` dans `src/models/task.js` en utilisant les schémas Mongoose :

// src/models/task.js
const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, 'Le titre de la tâche est obligatoire.'],
    trim: true // Enlever les espaces superflus au début et à la fin
  },
  description: {
    type: String,
    trim: true,
    default: '' // Valeur par défaut si non fourni
  },
  completed: {
    type: Boolean,
    required: true,
    default: false
  }
}, {
  timestamps: true // Ajoute automatiquement createdAt et updatedAt
});

const Task = mongoose.model('Task', taskSchema);

module.exports = Task;

Ce schéma définit la structure, les types, les validations de base (`required`) et active les timestamps automatiques (`createdAt`, `updatedAt`).

Création des routes et des contrôleurs (logique CRUD)

Définissons nos routes Express dans `src/routes/taskRoutes.js` et implémentons la logique pour chaque endpoint CRUD.

// src/routes/taskRoutes.js
const express = require('express');
const Task = require('../models/task'); // Importer notre modèle Task

const router = express.Router();

// POST /tasks - Créer une nouvelle tâche
router.post('/tasks', async (req, res) => {
  const task = new Task(req.body);
  try {
    await task.save();
    res.status(201).send(task); // 201 Created
  } catch (error) {
    res.status(400).send({ error: error.message }); // 400 Bad Request
  }
});

// GET /tasks - Lire toutes les tâches
router.get('/tasks', async (req, res) => {
  try {
    const tasks = await Task.find({});
    res.status(200).send(tasks); // 200 OK
  } catch (error) {
    res.status(500).send({ error: 'Erreur serveur lors de la récupération des tâches.' }); // 500 Internal Server Error
  }
});

// GET /tasks/:id - Lire une tâche par ID
router.get('/tasks/:id', async (req, res) => {
  const _id = req.params.id;
  try {
    // Vérifier si l'ID est un ObjectId valide (optionnel mais recommandé)
    if (!mongoose.Types.ObjectId.isValid(_id)) {
        return res.status(400).send({ error: 'ID de tâche invalide.' });
    }
    const task = await Task.findById(_id);
    if (!task) {
      return res.status(404).send({ error: 'Tâche non trouvée.' }); // 404 Not Found
    }
    res.status(200).send(task); // 200 OK
  } catch (error) {
    res.status(500).send({ error: 'Erreur serveur lors de la récupération de la tâche.' });
  }
});

// PATCH /tasks/:id - Mettre à jour une tâche par ID
router.patch('/tasks/:id', async (req, res) => {
  const _id = req.params.id;
  const updates = Object.keys(req.body); // Clés à mettre à jour
  const allowedUpdates = ['title', 'description', 'completed']; // Champs autorisés à la mise à jour
  const isValidOperation = updates.every((update) => allowedUpdates.includes(update));

  if (!isValidOperation) {
    return res.status(400).send({ error: 'Mise à jour invalide ! Champs non autorisés.' });
  }
   if (!mongoose.Types.ObjectId.isValid(_id)) {
        return res.status(400).send({ error: 'ID de tâche invalide.' });
    }

  try {
    // Utiliser findByIdAndUpdate avec { new: true, runValidators: true }
    const task = await Task.findById(_id);

    if (!task) {
      return res.status(404).send({ error: 'Tâche non trouvée.' });
    }

    // Appliquer les mises à jour manuellement pour déclencher les hooks Mongoose si besoin
    updates.forEach((update) => task[update] = req.body[update]);
    await task.save(); // Déclenche les validations du schéma

    res.status(200).send(task); // 200 OK
  } catch (error) {
    res.status(400).send({ error: error.message }); // Erreurs de validation ou autres
  }
});

// DELETE /tasks/:id - Supprimer une tâche par ID
router.delete('/tasks/:id', async (req, res) => {
  const _id = req.params.id;
   if (!mongoose.Types.ObjectId.isValid(_id)) {
        return res.status(400).send({ error: 'ID de tâche invalide.' });
    }

  try {
    const task = await Task.findByIdAndDelete(_id);
    if (!task) {
      return res.status(404).send({ error: 'Tâche non trouvée.' });
    }
    // Optionnel: renvoyer la tâche supprimée ou juste un succès
    // res.status(200).send(task); 
    res.status(204).send(); // 204 No Content (standard pour DELETE réussi)
  } catch (error) {
    res.status(500).send({ error: 'Erreur serveur lors de la suppression de la tâche.' });
  }
});

// IMPORTANT: Importer mongoose ici aussi si vous utilisez ObjectId.isValid
const mongoose = require('mongoose');

module.exports = router;

Cette implémentation utilise `async/await` pour gérer l'asynchronisme des opérations de base de données et inclut une gestion basique des erreurs avec les codes de statut HTTP appropriés.

Mise en place du serveur Express

Enfin, assemblons le tout dans notre fichier serveur principal `src/server.js` :

// src/server.js
require('dotenv').config(); // Charger .env en premier
const express = require('express');
const connectDB = require('./db/mongoose'); // Notre fonction de connexion DB
const taskRouter = require('./routes/taskRoutes'); // Notre routeur pour les tâches

// Initialiser l'application Express
const app = express();
const port = process.env.PORT || 3000; // Utiliser le port de .env ou 3000 par défaut

// Connexion à la base de données
connectDB();

// Middleware pour parser le JSON des requêtes
app.use(express.json());

// Monter le routeur des tâches sur la route / (ou /api/v1 si vous préférez)
app.use(taskRouter); 

// Gestion simple d'erreur 404 pour les routes non trouvées
app.use((req, res, next) => {
    res.status(404).send({ error: 'Route non trouvée' });
});

// Middleware de gestion d'erreur global (très basique)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send({ error: 'Quelque chose s\'est mal passé !' });
});

// Démarrer le serveur
app.listen(port, () => {
  console.log(`Serveur démarré et écoute sur le port ${port}`);
});

Vous pouvez maintenant lancer votre serveur en mode développement avec `npm run dev`. Si votre base de données MongoDB est en cours d'exécution (localement ou sur Atlas) et que l'URL dans `.env` est correcte, vous devriez voir les messages de connexion et de démarrage du serveur. Vous pouvez commencer à tester votre API avec un client HTTP comme Postman ou Insomnia !

Nous avons maintenant une API RESTful fonctionnelle pour les opérations CRUD de base sur nos tâches. Les prochaines étapes consisteront à ajouter des tests pour garantir sa fiabilité et à la documenter pour faciliter son utilisation.