Se connecter à Flutter avec Node js MySQL
Se connecter à Flutter avec Node js MySQL
-
Objectif
- À la fin de ce tutoriel, vous serez capable de :
- Créer une base de données MySQL simple pour l’authentification.
- Écrire un backend Node.js pour la connexion à MySQL.
- Échanger des données entre Flutter et Node.js via HTTP.
- Créer une interface de connexion Flutter avec vérification côté serveur.
-
Présentation
- Ce tutoriel explique comment connecter une application Flutter à une base de données MySQL à l’aide d’un backend Node.js. Flutter ne peut pas accéder directement à MySQL, d’où l’importance de l’API Node.js.
-
Gestion des projets et dépendances
-
Prérequis à installer
-
Outils généraux
- Système d’exploitation : Windows, Linux ou macOS
- Connexion Internet pour installer les dépendances (npm, Flutter)
-
Backend – Node.js / MySQL
-
Qu’est-ce que Node.js ?
- Node.js est un environnement d’exécution (runtime) open-source, cross-platform, conçu pour exécuter du code JavaScript en dehors du navigateur. Il n’est ni un langage ni un framework, mais un moteur complet qui repose sur le V8 de Google Chrome.
- Contrairement au JavaScript classique limité aux APIs du navigateur et au DOM, Node.js donne un accès direct au système de fichiers, aux sockets réseau, aux processus et aux flux de données.
- Cette architecture a popularisé le développement full-stack JavaScript, permettant aux développeurs d’utiliser la même syntaxe, les mêmes outils (type ESLint, Prettier, TypeScript) et les mêmes structures de données (JSON) sur le frontend et le backend, ce qui réduit considérablement la complexité des projets modernes.
-
Comment fonctionne-t-il ?
- Le moteur V8 : Développé par Google, V8 compile le code JavaScript en langage machine natif via une optimisation JIT (Just-In-Time). Cette technique supprime la surcharge d’un interpréteur traditionnel et garantit des performances d’exécution proches de langages compilés comme C++. V8 gère également la gestion automatique de la mémoire (Garbage Collector), ce qui rend le développement plus sûr et moins sujet aux fuites de mémoire.
- Modèle Non-bloquant (I/O Asynchrone) : Node.js traite les opérations coûteuses (requêtes SQL, appels HTTP, lecture de fichiers) de manière asynchrone. Au lieu de geler le thread principal en attendant une réponse, il délègue ces tâches à
libuv(une bibliothèque C++ qui gère un pool de threads) et continue d’exécuter le reste du code. Dès qu’une opération se termine, le résultat est renvoyé via un callback, une promesse ou unasync/await. Ce modèle évite les blocages et maximise l’utilisation du CPU. - Event Loop (Boucle d’événements) : C’est le cœur battant de Node.js. Fonctionnant en arrière-plan, elle scanne continuellement des files d’attente spécifiques (timers, I/O, pending callbacks, check, close) et exécute les callbacks prêts à être traités. Grâce à ce modèle mono-threadé intelligent, Node.js peut gérer des milliers de connexions simultanées avec une empreinte mémoire minimale, ce qui le rend exceptionnellement adapté aux applications en temps réel (chat, notifications push, streaming de données).
-
Pourquoi utiliser Node.js avec Flutter ?
- L’intermédiaire sécurisé (Architecture API) : Connecter directement une application mobile Flutter à MySQL est une mauvaise pratique : cela exposerait vos identifiants en dur, rendrait l’app vulnérable aux injections SQL et compliquerait la gestion des mises à jour de schéma. Node.js agit comme un backend intermédiaire (API REST ou GraphQL) qui centralise la logique métier, valide les entrées, gère l’authentification (JWT, OAuth), applique des limites de requêtes (rate limiting) et communique de manière chiffrée avec la base de données.
- Performance & Évolutivité : Flutter excelle dans le rendu UI fluide (60/120 FPS), tandis que Node.js excelle dans la gestion d’I/O réseau. Sa capacité à maintenir des milliers de connexions persistantes (WebSockets, HTTP/2) avec une faible latence en fait le partenaire idéal pour synchroniser des données en temps réel, gérer des uploads de fichiers ou scaler horizontalement via des clusters (
clusterouPM2) lorsque la charge utilisateur augmente. - Écosystème NPM & Productivité : npm est le plus grand registre de paquets au monde. Des bibliothèques comme
express(routage & middleware),mysql2ouPrisma(ORM avancé),bcrypt(chiffrement sécurisé) etjsonwebtokenpermettent de monter une API robuste en quelques heures. De plus, l’usage de TypeScript côté backend offre une validation statique des types, facilitant le débogage et la maintenance à long terme de la couche API qui alimente votre app Flutter. -
Vérification après installation
- Si Node.js n’est pas encore installé : Rendez-vous sur le site officiel de Node.js et téléchargez l’installateur correspondant à votre système (Windows, macOS ou Linux). Privilégiez systématiquement la version LTS (Long Term Support) pour bénéficier d’une stabilité optimale et de correctifs de sécurité à long terme. L’installation est guidée et inclut automatiquement
npm. - Ouvrez un terminal (Invite de commandes, PowerShell, Bash ou Zsh) et exécutez les commandes ci-dessous pour confirmer que l’environnement est correctement configuré et prêt pour le développement :
node -v: Affiche la version installée. Pour tout projet en production ou en équipe, privilégiez une version LTS (ex: v20 ou v22). Vous pouvez gérer plusieurs versions simultanément sur votre machine grâce à des gestionnaires commenvm(Node Version Manager) oufnm.npm -v: Vérifie la version du gestionnaire de paquets intégré. npm est indispensable pour installer les dépendances (npm install express mysql2), exécuter des scripts personnalisés (npm run dev) et gérer les versions des modules via le fichierpackage.json. En cas d’erreur de commande non reconnue, vérifiez que le répertoire d’installation de Node.js a bien été ajouté à votre variable d’environnement systèmePATH.- Ouvrez un terminal (Invite de commandes, PowerShell, Bash ou Zsh) et exécutez les commandes ci-dessous pour confirmer que l’environnement est correctement configuré et prêt pour le développement :
node -v: Affiche la version installée. Pour tout projet en production ou en équipe, privilégiez une version LTS (Long Term Support, ex: v20 ou v22) qui bénéficie de correctifs de sécurité et d’une stabilité garantie sur plusieurs années. Vous pouvez gérer plusieurs versions facilement avec des outils commenvmoufnm.npm -v: Vérifie la version du gestionnaire de paquets intégré. npm est indispensable pour installer les dépendances (npm install express mysql2), exécuter des scripts personnalisés (npm run dev) et gérer les versions des modules via le fichierpackage.json. En cas d’erreur, vérifiez que le chemin d’installation de Node.js est bien ajouté à votre variable d’environnementPATH.- MySQL Server
- MySQL 8.x recommandé
- Ou XAMPP / WAMP / MAMP (solution simple pour les étudiants)
- Inclut phpMyAdmin pour gérer la base de données
- Vérification :
- Client de test API (recommandé)
- Postman → tester les routes
/login,/clients - Ou Thunder Client (extension VS Code)
-
Frontend – Flutter
- Flutter SDK
- Téléchargement : https://flutter.dev
- Ajouter Flutter au PATH système
- Vérification :
- Dépendances Flutter utilisées
- Package HTTP pour communiquer avec l’API
- Émulateur / Appareil réel
- Android Emulator (Android Studio)
- OU téléphone Android (mode développeur + débogage USB)
- Important :
- Android Emulator → utiliser
http://10.0.2.2:3000 - Téléphone réel → utiliser l’IP locale du PC (ex :
http://192.168.1.10:3000) -
Éditeur de code (obligatoire)
- Visual Studio Code (recommandé)
- Extensions utiles :
- Flutter
- Dart
- Node.js Snippets
- Thunder Client (tests API)
-
Connaissances préalables recommandées
- ✅ Bases de JavaScript
- ✅ Notions de SQL (SELECT, INSERT, UPDATE, DELETE)
- ✅ Bases du HTTP (GET, POST, PUT, DELETE)
- ✅ Logique MVC / API REST (notions)
-
Configuration du projet
-
Étape 1 : Créer la base de données MySQL
- Créer la base de données :
- Créer la table
utilisateurs: - Créer la table
client: -
Étape 2 : Créer le dossier du projet
-
Créer le dossier du projet
- Clique droit dans le dossier où tu veux travailler → Nouveau dossier
- Nomme-le par exemple : mon_api_node
- Clique droit sur ce dossier → Ouvrir avec Terminal (ou PowerShell ou Invite de commandes)
-
Initialiser le projet Node.js
- Dans le terminal ouvert dans le dossier mon_api_node, tape :
npm init -y - Normalement, tu dois taper :
npm init… et répondre à plusieurs questions : nom du projet, version, description, auteur etc. - Avec
npm init -y, tout est automatique ! - Le -y signifie « yes » à toutes les questions.
- Cela crée un fichier package.json.
-
Installer les modules nécessaires
- Toujours dans le terminal :
npm install express mysql2 bcrypt body-parser - Cette commande installe quatre modules Node.js nécessaires pour développer un serveur avec Node.js.
- 1. express
- C’est le framework le plus utilisé pour créer des serveurs web avec Node.js.
- Il permet de gérer les routes, les requêtes HTTP, les réponses, etc.
- 2. mysql2
- C’est un module permettant de connecter Node.js à une base de données MySQL.
- Plus rapide et moderne que le module mysql.
- Il permet d’exécuter des requêtes SQL depuis votre serveur Node.js.
- 3. bcrypt
- Sert à chiffrer (hasher) les mots de passe avant de les enregistrer dans votre base de données.
- Très important pour la sécurité.
- 4. body-parser
- Permet à Express de lire les données envoyées par un formulaire ou par POST.
- Aujourd’hui, Express intègre déjà une partie de body-parser, mais le module reste utilisé.
-
Étape 3 : Créer les fichiers un par un
-
Créer le fichier
config.js - Dans ton dossier flutter_nodeJS créer le fichier config.js :
- Dans Node.js,
module.exportspermet d’envoyer des données (ou fonctions) vers d’autres fichiers. - Cela signifie : »Rendre ces valeurs disponibles pour être utilisées ailleurs dans mon projet. »
-
Créer le fichier
db.js: - Ce code permet d’établir une connexion à une base de données MySQL en utilisant la bibliothèque mysql2 dans un projet Node.js.
- On importe la bibliothèque mysql2 qui fournit des fonctions pour interagir avec une base de données MySQL.
- On importe un fichier de configuration local config.js (ou .json) qui contient les paramètres de connexion à la base de données (hôte, utilisateur, mot de passe, etc.).
- On crée un objet connection qui représente la connexion à la base de données MySQL.
- Les paramètres utilisés sont :
- host: l’adresse du serveur MySQL (ex : localhost ou une IP)
- user: le nom d’utilisateur MySQL
- password: le mot de passe associé
- database: le nom de la base de données à utiliser
- charset: le jeu de caractères (ex : utf8mb4)
- Ici, on lance la connexion vers MySQL.
- Si la connexion échoue, on affiche l’erreur dans la console et on quitte le processus Node.js (process.exit(1)).
- Sinon, on affiche un message de succès.
- On exporte l’objet connection pour pouvoir le réutiliser dans d’autres fichiers du projet (par exemple, pour exécuter des requêtes SQL).
-
Créer le fichier
login.js: - Ce code crée un serveur web avec Express qui écoute sur le port 3000 et propose un endpoint POST /login permettant à un utilisateur de se connecter via email et mot de passe. Les mots de passe sont sécurisés avec bcrypt.
- express pour créer le serveur HTTP.
- db est un module local qui gère la connexion à la base de données (MySQL).
- bcrypt pour comparer les mots de passe hashés.
- body-parser pour parser les requêtes JSON.
- On initialise une instance Express app.
- Middleware qui ajoute des headers CORS pour autoriser toutes les origines à accéder à l’API (utile pour le développement et appels cross-origin).
- Il permet aussi de dire que l’en-tête Content-Type est autorisé dans les requêtes.
- Le next() permet de passer au middleware suivant.
- Route POST /login : on récupère email et password depuis le corps JSON de la requête.
- Si l’un des deux est manquant, on répond immédiatement avec un message d’erreur.
- On interroge la base MySQL via db.query pour chercher un utilisateur par email.
- Si erreur ou aucun résultat, on renvoie un message d’authentification échouée.
- On récupère le premier utilisateur trouvé.
- On compare le mot de passe envoyé avec le hash stocké en base via bcrypt.compareSync.
- Si le mot de passe ne correspond pas, on renvoie une erreur.
- Si tout est valide, on renvoie un objet JSON avec success: true et les infos utilisateur (id, email, rôles convertis depuis JSON string).
- Dans votre terminal tapez :
node create_user.js - Tester si le serveur Node.js démarre
- PS C:\wamp64\www\flutter\mon_api_node> node db.js
Connexion MySQL réussie ! -
Créer le fichier
insert_user.jspour ajouter un utilisateur : - On commence par l’importation des modules
app.use(express.json());:permettre de lire automatiquement le JSON des requêtes- CORS = Cross-Origin Resource Sharing
- origin: ‘*’:N’importe qui peut appeler l’API (ou Flutter).
- methods:Liste des méthodes autorisées (POST, GET…).
- allowedHeaders: Autorise l’envoi de Content-Type (important pour JSON).
- credentials:Autorise l’envoi de cookies (pas obligatoire ici).
-
Tester l’API avant de toucher à Flutter.
- Voici comment faire ton test étape par étape pour être sûr que ça marche :
- Lance ton serveur
- Dans ton terminal, à l’emplacement de tes fichiers, tape :
node login.js - Tu devrais voir s’afficher : Serveur Node.js en écoute sur http://localhost:3000.
- Configure Postman
- Ouvre Postman et crée une nouvelle requête avec ces paramètres précis :
- Méthode : Sélectionne POST (et non GET).
- URL : http://localhost:3000/login
- Onglet Body :
- Sélectionne raw.
- Dans le menu déroulant à droite, change « Text » pour JSON.
- Contenu du Body : Copie ce JSON (en utilisant un email et un mot de passe qui existent dans ta base MySQL) :
- Comme tu utilises bcrypt et une base de données MySQL, vérifie ces trois points si le test échoue :
- Ta base de données : Ton serveur MySQL doit être allumé (via XAMPP, WAMP ou Docker).
- Le mot de passe dans MySQL : Le mot de passe dans ta table utilisateurs doit être haché avec bcrypt. Si tu as écrit « 123456 » en texte clair dans ta base, bcrypt.compareSync retournera false.
- Le champ Roles : Dans ton code, tu fais JSON.parse(user.roles). Assure-toi que dans ta base de données, la colonne roles contient bien une chaîne au format JSON (ex: [« USER »] ou [« ADMIN »]), sinon le serveur va crasher.
- Pourquoi c’est important de tester maintenant ?
- En testant avec Postman, tu isoles les problèmes.
- Si ça marche sur Postman mais pas sur Flutter : le problème vient de ton code Dart (ou de l’adresse IP de l’émulateur).
- Si ça ne marche pas sur Postman : le problème vient de ton code Node.js ou de ta connexion MySQL.
-
Étape 4 : Fichiers server.js
-
Créer le fichier
server.jsCe fichier initialise le serveur, configure la sécurité (cors) et rassemble toutes les routes. C’est le point d’entrée unique. -
Étape 4 : Fichiers auth.routes.js
-
Créer le fichier
auth.routes.jsCe fichier ne s’occupe que de la sécurité : connexion (login), vérification des mots de passe et rôles. -
Créer le fichier
client.routes.jsCe fichier ne s’occupe que des données métiers : afficher, ajouter, modifier ou supprimer des clients. -
Étape 5 : Lancer les scripts
- Dans le terminal, tu fais :
- Dans votre dossier …\flutter_nodeJs, exécutez :
npm install cors - Pour insérer un utilisateur test :
node insert_user_index.js - Pour lancer le serveur :
node login.js -
Créer le projet Flutter
- Créer le projet Flutter :
- Ajouter la dépendance HTTP dans
pubspec.yaml: -
Étape 4 : Créer l’interface de connexion Flutter
- Description : Ce fichier est le point d’entrée de votre application Flutter. Il lance le widget
MainAppqui affiche directement la page de connexionLoginPage. -
Créer le fichier de connexion
- Description : Ce fichier gère l’interface de connexion utilisateur. Lorsqu’un utilisateur saisit ses identifiants, l’application envoie une requête POST à l’API
Node.js(route/login). Si les identifiants sont valides, il est redirigé vers une autre page. -
Étape 5 : Définir un modèle de données (Utilisateur)
- Description : Cette classe représente un utilisateur. Elle permet de convertir les données reçues du backend Node.js (en JSON) en objet Dart, et inversement.
-
Étape 5 : Créer le fichier
home_page.dart - Ce fichier Flutter est la page principale de l’application. Il permet d’afficher la liste des clients, d’en ajouter, modifier ou supprimer via l’API Node.js.
-
Exercice Pratique : Gestion du Stock (CRUD)
- Étape 1 : Base de données
- Étape 2 : Backend (API Node.js)
GET /produits: Lister tous les produits.POST /produits: Ajouter un nouveau produit.PUT /produits/:id: Modifier un produit existant.DELETE /produits/:id: Supprimer un produit.- Création du contrôleur de routes (
produit.route.js) - Créez un nouveau fichier nommé produit.route.js. Dans ce fichier, vous devez :
- Importer express et le router.
- Importer votre connexion à la base de données (db.js).
- Écrire les 4 fonctions (GET, POST, PUT, DELETE) en adaptant la logique utilisée pour les clients à la structure de la table produits.
- Exporter le router à la fin du fichier.
- Enregistrement dans le serveur principal (
server.js) - Une fois votre fichier de routes prêt, vous devez « le brancher » sur votre application pour qu’il soit accessible. Modifiez votre fichier server.js :
- Importez le fichier : const produitRoutes = require(‘./produit.route’);
- Déclarez l’utilisation du module : app.use(‘/api’, produitRoutes);
- Étape 3 : Frontend (Application Flutter)
- Affichage des produits sous forme de ListView.
- Formulaire de saisie avec validation des données.
- Gestion des images via l’URL
image_url. -
Fichiers réalisés
- connexion.php
- login.php
- main.dart
- pubspec.yaml
- Base de données MySQL : base_test / table users
| Commande | Description |
|---|---|
| npm init | Initialise un nouveau projet Node.js (crée package.json) |
| npm init -y | Initialise avec les valeurs par défaut |
| npm install | Installe toutes les dépendances du package.json |
| npm install <package> | Installe un package spécifique |
| npm install –save <package> | Installe et ajoute à dependencies |
| npm install –save-dev <package> | Installe et ajoute à devDependencies |
| npm install -g <package> | Installe globalement |
| npm update | Met à jour tous les packages |
| npm update <package> | Met à jour un package spécifique |
| npm uninstall <package> | Désinstalle un package |
| npm audit | Vérifie les vulnérabilités de sécurité |
| npm audit fix | Corrige automatiquement les vulnérabilités |
| npm list | Liste les packages installés |
| npm list –depth=0 | Liste seulement les packages de premier niveau |
| npm outdated | Vérifie les packages obsolètes |
| npm run <script> | Exécute un script défini dans package.json |
mysql --version
flutter --version
flutter doctor
flutter pub add http
CREATE DATABASE base_test;
CREATE TABLE `utilisateurs` (
`id` int NOT NULL AUTO_INCREMENT,
`email` varchar(180) NOT NULL,
`roles` json NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UNIQ_EMAIL` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `client` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`family` varchar(100) NOT NULL,
`age` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4;
module.exports = {
DB_HOST: '127.0.0.1',
DB_NAME: 'base_test',
DB_USER: 'root',
DB_PASSWORD: '',
DB_CHARSET: 'utf8mb4'
};
db.js
const mysql = require('mysql2');
const config = require('./config');
const connection = mysql.createConnection({
host: config.DB_HOST,
user: config.DB_USER,
password: config.DB_PASSWORD,
database: config.DB_NAME,
charset: config.DB_CHARSET
});
connection.connect(err => {
if (err) {
console.error("Erreur de connexion MySQL :", err.message);
process.exit(1);
} else {
console.log("Connexion MySQL réussie !");
}
});
module.exports = connection;
const mysql = require('mysql2');
const config = require('./config');
const connection = mysql.createConnection({
host: config.DB_HOST,
user: config.DB_USER,
password: config.DB_PASSWORD,
database: config.DB_NAME,
charset: config.DB_CHARSET
});
connection.connect(err => {
if (err) {
console.error("Erreur de connexion MySQL :", err.message);
process.exit(1);
} else {
console.log("Connexion MySQL réussie !");
}
});
module.exports = connection;
login.js
const express = require('express');
const db = require('./db');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
next();
});
app.post('/login', (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.json({ success: false, message: "Email ou mot de passe manquant." });
}
db.query("SELECT * FROM utilisateurs WHERE email = ?", [email], (err, results) => {
if (err || results.length === 0) {
return res.json({ success: false, message: "Identifiants invalides" });
}
const user = results[0];
const passwordMatch = bcrypt.compareSync(password, user.password);
if (!passwordMatch) {
return res.json({ success: false, message: "Mot de passe incorrect" });
}
return res.json({
success: true,
user: {
id: user.id,
email: user.email,
roles: JSON.parse(user.roles)
}
});
});
});
app.listen(3000, () => console.log("Serveur Node.js en écoute sur http://localhost:3000"));
const express = require('express');
const db = require('./db');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
const app = express();
On importe les modules nécessaires :
app.use(bodyParser.json());
On utilise body-parser pour que Express puisse comprendre les données JSON envoyées dans le corps des requêtes (important pour POST).
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
next();
});
app.post('/login', (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.json({ success: false, message: "Email ou mot de passe manquant." });
}
db.query("SELECT * FROM utilisateurs WHERE email = ?", [email], (err, results) => {
if (err || results.length === 0) {
return res.json({ success: false, message: "Identifiants invalides" });
}
const user = results[0];
const passwordMatch = bcrypt.compareSync(password, user.password);
if (!passwordMatch) {
return res.json({ success: false, message: "Mot de passe incorrect" });
}
return res.json({
success: true,
user: {
id: user.id,
email: user.email,
roles: JSON.parse(user.roles)
}
});
});
});
cd wamp64/www/flutter/mon_api_node
node db.js
insert_user_index.js
const db = require('./db');
const bcrypt = require('bcrypt');
const email = 'nodejs@example.com';
const password = bcrypt.hashSync('123456', 10);
const roles = JSON.stringify(['ROLE_USER']);
db.query(
"INSERT INTO utilisateurs (email, password, roles) VALUES (?, ?, ?)",
[email, password, roles],
(err, result) => {
if (err) {
console.error("Erreur d'insertion :", err.message);
} else {
console.log("Utilisateur ajouté avec ID :", result.insertId);
}
process.exit();
}
);
-
✔ express: Framework permettant de créer un serveur HTTP facilement.
✔ cors: Module qui permet d’autoriser les requêtes venant d’autres applications (comme Flutter).
✔ app = express(): On crée l’application Express, qui représente notre serveur.
-
Sans cette ligne : Express ne comprend pas le JSON envoyé par Flutter.
req.body serait undefined.
JSON
{
"email": "nodejs@example.com",
"password": "123456"
}

server.js
// server.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
// 1. Importation des modules de routes
const authRoutes = require('./auth.routes');
//const clientRoutes = require('./client.routes');
const app = express();
// Configuration des Middlewares
app.use(cors());
app.use(bodyParser.json());
// 2. Montage des routes (Les routes /login et /clients sont maintenant disponibles)
app.use(authRoutes);
app.use(clientRoutes);
// -------------------- SERVER START --------------------
const PORT = 3000;
app.listen(PORT, () => console.log(`Serveur Node.js en écoute sur http://localhost:${PORT}`));
auth.routes.js
// -------------------- IMPORTATIONS --------------------
// On importe le framework Express pour créer des routes HTTP
const express = require('express');
// On crée un router Express. Le router permet de créer des routes modulaires,
// séparées du fichier principal de l'application.
const router = express.Router(); // Utilisation du Router d'Express
// On importe le module de connexion à la base de données
// Il doit contenir la configuration et la méthode query pour exécuter des requêtes SQL.
const db = require('./db');
// On importe bcrypt pour le hachage des mots de passe
// Il permet de comparer le mot de passe saisi avec le mot de passe stocké haché.
const bcrypt = require('bcrypt');
// -------------------- ROUTE LOGIN --------------------
// Route POST pour la connexion d'un utilisateur
router.post('/login', (req, res) => {
// Récupération de l'email et du mot de passe envoyés dans le corps de la requête
const { email, password } = req.body;
// Vérification que l'utilisateur a bien fourni un email et un mot de passe
if (!email || !password) {
// Si l'email ou le mot de passe est manquant, on renvoie un message d'erreur
return res.json({ success: false, message: "Email ou mot de passe manquant." });
}
// Recherche de l'utilisateur dans la base de données par email
db.query("SELECT * FROM utilisateurs WHERE email = ?", [email], (err, results) => {
// Si une erreur SQL se produit ou si aucun utilisateur n'est trouvé
if (err || results.length === 0) {
return res.json({ success: false, message: "Identifiants invalides" });
}
// On récupère le premier utilisateur trouvé
const user = results[0];
// Vérification du mot de passe : on compare le mot de passe saisi avec le mot de passe haché
const passwordMatch = bcrypt.compareSync(password, user.password);
// Si le mot de passe ne correspond pas, on renvoie un message d'erreur
if (!passwordMatch) {
return res.json({ success: false, message: "Mot de passe incorrect" });
}
// Gestion des rôles de l'utilisateur
let rolesArray;
try {
// On essaie de parser le champ roles qui est stocké en JSON dans la base
rolesArray = JSON.parse(user.roles); // parse la chaîne JSON
// Si le résultat n'est pas un tableau, on le transforme en tableau
if (!Array.isArray(rolesArray)) rolesArray = [rolesArray];
} catch (e) {
// Si ce n'est pas du JSON valide, on crée un tableau contenant directement la valeur
rolesArray = [user.roles];
}
// Réponse JSON en cas de succès de la connexion
// On renvoie l'ID, l'email et les rôles de l'utilisateur
return res.json({
success: true,
user: {
id: user.id,
email: user.email,
roles: rolesArray
}
});
});
});
// Export du router pour pouvoir l'utiliser dans le fichier principal de l'application
module.exports = router;
client.routes.js
// -------------------- IMPORTATIONS --------------------
// On importe le framework Express pour créer des routes HTTP
const express = require('express');
// Création d'un router Express pour définir des routes modulaires
const router = express.Router(); // Utilisation du Router d'Express
// On importe le module de connexion à la base de données
const db = require('./db');
// -------------------- ROUTE GET /clients --------------------
// Récupérer la liste de tous les clients
router.get('/clients', (req, res) => {
// Requête SQL pour sélectionner tous les clients, triés par ID décroissant
const sql = "SELECT * FROM client ORDER BY id DESC";
// Exécution de la requête
db.query(sql, (err, results) => {
if (err) {
// Si erreur SQL, renvoyer un statut 500 et un message d'erreur
return res.status(500).json({ error: "Erreur serveur" });
}
// Sinon, renvoyer les résultats (liste des clients) en JSON
res.json(results);
});
});
// -------------------- ROUTE POST /add-client --------------------
// Ajouter un nouveau client
router.post('/add-client', (req, res) => {
// Récupération des données envoyées dans le corps de la requête
// On fournit des valeurs par défaut si les champs sont absents
const { name = "", family = "", age = 0 } = req.body;
// Vérification que le nom et le prénom ne sont pas vides
if (!name.trim() || !family.trim()) {
return res.status(400).json({ message: "Nom et prénom requis" });
}
// Requête SQL pour insérer un nouveau client
const sql = "INSERT INTO client (name, family, age) VALUES (?, ?, ?)";
db.query(sql, [name, family, age], (err, result) => {
if (err) {
console.error("Erreur SQL:", err);
return res.status(500).json({ message: "Erreur BD" });
}
// Envoi d'une réponse avec l'ID du nouveau client et un message de succès
res.status(201).json({ id: result.insertId, message: "Client ajouté" });
});
});
// -------------------- ROUTE PUT /clients/:id --------------------
// Mettre à jour les informations d'un client existant
router.put('/clients/:id', (req, res) => {
// Récupération de l'ID du client depuis les paramètres de l'URL
const { id } = req.params;
// Récupération des nouvelles valeurs depuis le corps de la requête
const { name, family, age } = req.body;
// Requête SQL pour mettre à jour le client
const sql = "UPDATE client SET name=?, family=?, age=? WHERE id=?";
db.query(sql, [name, family, age, id], (err, result) => {
if (err) return res.status(500).json({ message: "Erreur mise à jour" });
// Si aucun client n'a été affecté, l'ID n'existe pas
if (result.affectedRows === 0) {
return res.status(404).json({ message: "Client introuvable" });
}
// Réponse de succès
res.json({ message: "Client mis à jour" });
});
});
// -------------------- ROUTE DELETE /clients/:id --------------------
// Supprimer un client existant
router.delete('/clients/:id', (req, res) => {
// Récupération de l'ID du client depuis les paramètres de l'URL
const { id } = req.params;
// Requête SQL pour supprimer le client
const sql = "DELETE FROM client WHERE id=?";
db.query(sql, [id], (err, result) => {
if (err) return res.status(500).json({ message: "Erreur suppression" });
// Si aucun client n'a été supprimé, l'ID n'existe pas
if (result.affectedRows === 0) {
return res.status(404).json({ message: "Client introuvable" });
}
// Réponse de succès
res.json({ message: "Client supprimé" });
});
});
// Export du router pour l'utiliser dans l'application principale
module.exports = router;
flutter create flutter_login_app
dependencies:
flutter:
sdk: flutter
http: ^0.13.5
Fichier : main.dart
import 'package:flutter/material.dart';
import 'login_page.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: LoginPage(),
);
}
}
Fichier login_page.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'home_page.dart'; // Import de ta page Home
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _emailCtrl = TextEditingController();
final _passwordCtrl = TextEditingController();
String errorMsg = '';
bool _isLoading = false;
bool _obscurePassword = true;
// Utilise l'IP de ta machine, ici la même que dans ta page Home
final String serverUrl = "http://10.0.2.2:3000/login";
Future<void> _login() async {
try {
setState(() {
errorMsg = "";
_isLoading = true;
});
final response = await http.post(
Uri.parse(serverUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': _emailCtrl.text,
'password': _passwordCtrl.text,
}),
);
print('Statut: ${response.statusCode}');
print('Corps: ${response.body}');
final data = jsonDecode(response.body);
// 🚀 CORRECTION ICI : Utilisez 'success' au lieu de 'status'
if (data['success'] == true) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
} else {
setState(() {
// Le message d'erreur est sous la clé 'message' si 'success' est false
errorMsg = data['message'] ?? 'Identifiants incorrects';
});
}
} catch (e) {
print('Erreur: $e');
setState(() {
errorMsg = 'Erreur de connexion au serveur.';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
/// HEADER
Text(
"Connexion",
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.indigo[700],
),
),
const SizedBox(height: 8),
Text(
"Bienvenue, connectez-vous pour continuer",
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 40),
/// EMAIL FIELD
TextField(
controller: _emailCtrl,
decoration: InputDecoration(
labelText: "Adresse email",
prefixIcon: const Icon(Icons.email),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (_) => setState(() => errorMsg = ""),
),
const SizedBox(height: 20),
/// PASSWORD FIELD
TextField(
controller: _passwordCtrl,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: "Mot de passe",
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined),
onPressed: () =>
setState(() => _obscurePassword = !_obscurePassword),
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (_) => setState(() => errorMsg = ""),
),
const SizedBox(height: 10),
/// FORGOT PASSWORD
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {},
child: Text(
"Mot de passe oublié ?",
style: TextStyle(color: Colors.indigo[600]),
),
),
),
const SizedBox(height: 20),
/// LOGIN BUTTON
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
"Se connecter",
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
const SizedBox(height: 15),
/// ERROR MESSAGE
if (errorMsg.isNotEmpty)
Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red[600]),
const SizedBox(width: 10),
Expanded(
child: Text(
errorMsg,
style: TextStyle(
color: Colors.red[700],
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const SizedBox(height: 30),
/// FOOTER
Center(
child: Text(
"Développé pour cours Flutter",
style: TextStyle(color: Colors.grey[500]),
),
),
],
),
),
),
);
}
}
Fichier : client_model.dart
class Client {
final int id;
final String name;
final String family;
final int age;
Client({
required this.id,
required this.name,
required this.family,
required this.age,
});
factory Client.fromJson(Map<String, dynamic> json) {
return Client(
id: json['id'] as int? ?? 0, // Gestion null-safe
name: (json['name'] ?? '') as String,
family: (json['family'] ?? '') as String,
age: (json['age'] ?? 0) as int,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'family': family,
'age': age,
};
}
Fichier : home_page.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'client_model.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _nameCtrl = TextEditingController();
final _familyCtrl = TextEditingController();
final _ageCtrl = TextEditingController();
String serverMsg = '';
final List<Client> _clientsList = [];
final String baseUrl = "http://10.0.2.2:3000";
// ------------------ INSERT ------------------
Future<void> _postRequest() async {
try {
final response = await http.post(
Uri.parse("$baseUrl/add-client"),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'name': _nameCtrl.text.trim(),
'family': _familyCtrl.text.trim(),
'age': int.tryParse(_ageCtrl.text) ?? 0,
}),
);
final res = jsonDecode(response.body);
serverMsg = response.statusCode == 201 ? "Client ajouté" : res["message"];
_clearForm();
await _getRequest();
setState(() {});
} catch (e) {
serverMsg = e.toString();
setState(() {});
}
}
// ------------------ LOAD LIST ------------------
Future<void> _getRequest() async {
try {
final response = await http.get(Uri.parse("$baseUrl/clients"));
if (response.statusCode == 200) {
final List jsonData = jsonDecode(response.body);
_clientsList.clear();
_clientsList.addAll(jsonData.map((e) => Client.fromJson(e)));
}
serverMsg = "Données chargées";
} catch (e) {
serverMsg = e.toString();
}
setState(() {});
}
// ------------------ DELETE ------------------
Future<void> _deleteRequest(int id) async {
try {
final response =
await http.delete(Uri.parse("$baseUrl/clients/$id"));
serverMsg =
response.statusCode == 200 ? "Client supprimé" : "Erreur suppression";
await _getRequest();
setState(() {});
} catch (e) {
serverMsg = e.toString();
setState(() {});
}
}
// ------------------ UPDATE ------------------
Future<void> _updateRequest(Client client) async {
try {
final response = await http.put(
Uri.parse("$baseUrl/clients/${client.id}"),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(client.toJson()),
);
serverMsg = response.statusCode == 200
? "Client mis à jour"
: "Erreur mise à jour";
await _getRequest();
setState(() {});
} catch (e) {
serverMsg = e.toString();
setState(() {});
}
}
void _clearForm() {
_nameCtrl.clear();
_familyCtrl.clear();
_ageCtrl.clear();
}
@override
void initState() {
super.initState();
_getRequest();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
// ------------------ APPBAR ------------------
appBar: AppBar(
title: const Text("Gestion des Clients"),
backgroundColor: Colors.indigo,
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
_clearForm();
_showAddDialog();
},
)
],
),
// ------------------ LIST ------------------
body: _buildClientList(),
floatingActionButton: FloatingActionButton(
onPressed: () {
_clearForm();
_showAddDialog();
},
backgroundColor: Colors.indigo,
child: const Icon(Icons.add),
),
);
}
// ------------------ LIST BUILDER ------------------
Widget _buildClientList() {
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _clientsList.length,
itemBuilder: (context, index) {
final client = _clientsList[index];
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
elevation: 3,
margin: const EdgeInsets.symmetric(vertical: 8),
child: ListTile(
title: Text(
"${client.name} ${client.family}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text("Âge : ${client.age}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// EDIT
IconButton(
icon: const Icon(Icons.edit, color: Colors.indigo),
onPressed: () {
_nameCtrl.text = client.name;
_familyCtrl.text = client.family;
_ageCtrl.text = client.age.toString();
_showUpdateDialog(client);
},
),
// DELETE
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _deleteRequest(client.id),
),
],
),
),
);
},
);
}
// ------------------ DIALOG AJOUT ------------------
void _showAddDialog() {
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: const Text("Ajouter un client"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_textField(_nameCtrl, "Nom", Icons.person),
const SizedBox(height: 10),
_textField(_familyCtrl, "Prénom", Icons.family_restroom),
const SizedBox(height: 10),
_textField(_ageCtrl, "Âge", Icons.cake,
keyboard: TextInputType.number),
],
),
actions: [
TextButton(
child: const Text("Annuler"),
onPressed: () => Navigator.pop(context),
),
ElevatedButton(
child: const Text("Ajouter"),
onPressed: () async {
await _postRequest();
Navigator.pop(context);
},
),
],
);
},
);
}
// ------------------ DIALOG UPDATE ------------------
void _showUpdateDialog(Client client) {
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: const Text("Modifier le client"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_textField(_nameCtrl, "Nom", Icons.person),
const SizedBox(height: 10),
_textField(_familyCtrl, "Prénom", Icons.family_restroom),
const SizedBox(height: 10),
_textField(_ageCtrl, "Âge", Icons.cake,
keyboard: TextInputType.number),
],
),
actions: [
TextButton(
child: const Text("Annuler"),
onPressed: () => Navigator.pop(context),
),
ElevatedButton(
child: const Text("Mettre à jour"),
onPressed: () async {
await _updateRequest(
Client(
id: client.id,
name: _nameCtrl.text,
family: _familyCtrl.text,
age: int.tryParse(_ageCtrl.text) ?? 0,
),
);
Navigator.pop(context);
},
),
],
);
},
);
}
// ------------------ TEXTFIELD DESIGN ------------------
Widget _textField(TextEditingController ctrl, String label, IconData icon,
{TextInputType keyboard = TextInputType.text}) {
return TextField(
controller: ctrl,
keyboardType: keyboard,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
}
L’objectif de cet exercice est de créer une architecture complète : un Backend avec Node.js et un Frontend mobile avec Flutter.
Préparez votre environnement MySQL et exécutez le script suivant pour créer la table :
CREATE TABLE `produits` (
`id` int NOT NULL AUTO_INCREMENT,
`nom` varchar(255) NOT NULL,
`description` text,
`prix` decimal(10,2) NOT NULL,
`quantite` int DEFAULT '0',
`image_url` varchar(500) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Créez une API REST permettant de gérer ces produits. Les points d’accès (endpoints) attendus sont :
Développez l’interface mobile pour consommer votre API :
