🔝 Retour au Sommaire
Les frameworks JavaScript modernes comme React, Vue.js et Angular ont révolutionné le développement d'interfaces web. Ils permettent de créer des applications web interactives, réactives et performantes. La bonne nouvelle ? Vous pouvez combiner la puissance de Delphi pour le backend avec ces frameworks pour le frontend !
L'idée centrale : Delphi gère les données et la logique métier (backend), tandis qu'un framework JavaScript crée une interface utilisateur moderne (frontend).
┌─────────────────────────────────┐
│ Frontend (Navigateur) │
│ ┌───────────────────────────┐ │
│ │ React / Vue / Angular │ │
│ │ (Interface utilisateur) │ │
│ └──────────┬────────────────┘ │
└─────────────┼───────────────────┘
│
│ API REST (JSON)
│ HTTP/HTTPS
│
┌─────────────┴───────────────────┐
│ Backend (Serveur) │
│ ┌───────────────────────────┐ │
│ │ Delphi / Object Pascal │ │
│ │ (Logique métier + BDD) │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
✅ Performance native - Code compilé rapide et efficace
✅ Accès base de données - FireDAC puissant et mature
✅ Frontière de confiance claire - Validation, autorisations et secrets
restent côté serveur (et non livrés au navigateur)
✅ Expertise existante - Capitaliser sur vos compétences
✅ Stabilité éprouvée - Plateforme fiable pour applications critiques
✅ Interface moderne - UX/UI à la pointe
✅ Réactivité - Mises à jour instantanées sans rechargement
✅ Écosystème riche - Milliers de composants prêts à l'emploi
✅ Communauté massive - Support et ressources abondantes
✅ Standards web - Technologies répandues et bien documentées
Cette architecture vous permet de :
- Utiliser Delphi pour ce qu'il fait de mieux : données et logique métier
- Profiter des frameworks JavaScript pour créer des interfaces exceptionnelles
- Évoluer indépendamment frontend et backend
- Recruter facilement des développeurs frontend JavaScript
┌──────────────────┐
│ Application JS │ (React/Vue/Angular)
│ (Frontend) │ Port 3000 ou 4200
└────────┬─────────┘
│
│ Appels API REST
│
┌────────┴─────────┐
│ API Delphi │ (Horse, RAD Server)
│ (Backend) │ Port 9000
└────────┬─────────┘
│
┌────────┴─────────┐
│ Base de données │
└──────────────────┘
Avantages :
- Séparation claire des responsabilités
- Développement parallèle possible
- Déploiement indépendant
- Scaling horizontal facilité
┌─────────────────────────────────┐
│ Serveur Web (Delphi) │
│ ┌───────────────────────────┐ │
│ │ API REST /api/* │ │
│ ├───────────────────────────┤ │
│ │ Fichiers JS / │ │
│ │ (build React/Vue) │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
Avantages :
- Déploiement simplifié
- Un seul serveur à gérer
- Pas de problème CORS
- Configuration réseau simplifiée
Créé par : Facebook/Meta
Type : Bibliothèque UI (pas un framework complet)
Philosophie : Composants réutilisables
Exemple simple React :
// Composant React qui affiche une liste de clients
import React, { useState, useEffect } from 'react';
function ClientsList() {
const [clients, setClients] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Appel à l'API Delphi
fetch('http://localhost:9000/api/clients')
.then(response => response.json())
.then(data => {
setClients(data);
setLoading(false);
})
.catch(error => {
console.error('Erreur:', error);
setLoading(false);
});
}, []);
if (loading) return <div>Chargement...</div>;
return (
<div>
<h1>Liste des clients</h1>
<table>
<thead>
<tr>
<th>Nom</th>
<th>Prénom</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{clients.map(client => (
<tr key={client.id}>
<td>{client.nom}</td>
<td>{client.prenom}</td>
<td>{client.email}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default ClientsList;Quand utiliser React :
- Interface complexe avec beaucoup d'interactions
- Besoin de flexibilité maximale
- Grande communauté et écosystème
- Nombreux développeurs disponibles sur le marché
Créé par : Evan You
Type : Framework progressif
Philosophie : Simple et intuitif
Exemple simple Vue.js :
<template>
<div>
<h1>Liste des clients</h1>
<div v-if="loading">Chargement...</div>
<table v-else>
<thead>
<tr>
<th>Nom</th>
<th>Prénom</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr v-for="client in clients" :key="client.id">
<td>{{ client.nom }}</td>
<td>{{ client.prenom }}</td>
<td>{{ client.email }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: 'ClientsList',
data() {
return {
clients: [],
loading: true
};
},
mounted() {
// Appel à l'API Delphi
fetch('http://localhost:9000/api/clients')
.then(response => response.json())
.then(data => {
this.clients = data;
this.loading = false;
})
.catch(error => {
console.error('Erreur:', error);
this.loading = false;
});
}
};
</script>Quand utiliser Vue.js :
- Courbe d'apprentissage douce
- Migration progressive d'application existante
- Documentation excellente en français
- Bonne productivité dès le départ
Créé par : Google
Type : Framework complet
Philosophie : Structure et conventions strictes
Exemple simple Angular :
// clients.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface Client {
id: number;
nom: string;
prenom: string;
email: string;
}
@Component({
selector: 'app-clients',
templateUrl: './clients.component.html'
})
export class ClientsComponent implements OnInit {
clients: Client[] = [];
loading = true;
constructor(private http: HttpClient) {}
ngOnInit() {
// Appel à l'API Delphi
this.http.get<Client[]>('http://localhost:9000/api/clients')
.subscribe({
next: (data) => {
this.clients = data;
this.loading = false;
},
error: (error) => {
console.error('Erreur:', error);
this.loading = false;
}
});
}
}<!-- clients.component.html -->
<div>
<h1>Liste des clients</h1>
<div *ngIf="loading">Chargement...</div>
<table *ngIf="!loading">
<thead>
<tr>
<th>Nom</th>
<th>Prénom</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let client of clients">
<td>{{ client.nom }}</td>
<td>{{ client.prenom }}</td>
<td>{{ client.email }}</td>
</tr>
</tbody>
</table>
</div>Quand utiliser Angular :
- Applications d'entreprise complexes
- Équipe habituée à TypeScript
- Besoin de structure stricte
- Applications à grande échelle
Pour que JavaScript puisse communiquer avec votre API Delphi, vous devez configurer CORS (Cross-Origin Resource Sharing).
program APIServer;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
Horse,
Horse.Jhonson, // Pour JSON
Horse.CORS, // Pour CORS
Horse.HandleException, // Gestion erreurs
System.JSON,
FireDAC.Comp.Client,
DataModuleUnit in 'DataModuleUnit.pas';
var
App: THorse;
// Route GET /api/clients
procedure GetClients(Req: THorseRequest; Res: THorseResponse; Next: TProc);
var
Query: TFDQuery;
JSONArray: TJSONArray;
JSONObject: TJSONObject;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := DMData.Connection;
Query.SQL.Text := 'SELECT id, nom, prenom, email FROM clients ORDER BY nom';
Query.Open;
JSONArray := TJSONArray.Create;
try
while not Query.Eof do
begin
JSONObject := TJSONObject.Create;
JSONObject.AddPair('id', TJSONNumber.Create(Query.FieldByName('id').AsInteger));
JSONObject.AddPair('nom', Query.FieldByName('nom').AsString);
JSONObject.AddPair('prenom', Query.FieldByName('prenom').AsString);
JSONObject.AddPair('email', Query.FieldByName('email').AsString);
JSONArray.Add(JSONObject);
Query.Next;
end;
Res.Send<TJSONArray>(JSONArray);
finally
// JSONArray sera libéré automatiquement
end;
finally
Query.Free;
end;
end;
// Route GET /api/clients/:id
procedure GetClient(Req: THorseRequest; Res: THorseResponse; Next: TProc);
var
Query: TFDQuery;
JSONObject: TJSONObject;
ClientID: Integer;
begin
// ⚠️ TryStrToInt évite une AccessViolation si /api/clients/abc est appelé.
// StrToInt lèverait une EConvertError → 500 au lieu de 400.
if not TryStrToInt(Req.Params['id'], ClientID) then
begin
Res.Status(400).Send('ID invalide');
Exit;
end;
Query := TFDQuery.Create(nil);
try
Query.Connection := DMData.Connection;
Query.SQL.Text := 'SELECT * FROM clients WHERE id = :id';
Query.ParamByName('id').AsInteger := ClientID;
Query.Open;
if Query.IsEmpty then
begin
Res.Status(404).Send('Client non trouvé');
Exit;
end;
JSONObject := TJSONObject.Create;
try
JSONObject.AddPair('id', TJSONNumber.Create(Query.FieldByName('id').AsInteger));
JSONObject.AddPair('nom', Query.FieldByName('nom').AsString);
JSONObject.AddPair('prenom', Query.FieldByName('prenom').AsString);
JSONObject.AddPair('email', Query.FieldByName('email').AsString);
JSONObject.AddPair('telephone', Query.FieldByName('telephone').AsString);
Res.Send<TJSONObject>(JSONObject);
finally
// JSONObject sera libéré automatiquement
end;
finally
Query.Free;
end;
end;
// Route POST /api/clients
procedure CreateClient(Req: THorseRequest; Res: THorseResponse; Next: TProc);
var
Query: TFDQuery;
Body: TJSONObject;
Response: TJSONObject;
NewID: Integer;
Nom, Prenom, Email: string;
begin
Body := Req.Body<TJSONObject>;
// ⚠️ TryGetValue<T> retourne un BOOLEAN et remplit un paramètre OUT.
// L'écriture `Body.TryGetValue<string>('nom').Trim.IsEmpty` ne
// compile pas — on extrait d'abord les valeurs via TryGetValue
// puis on les valide.
if not Body.TryGetValue<string>('nom', Nom) then Nom := '';
if not Body.TryGetValue<string>('prenom', Prenom) then Prenom := '';
if not Body.TryGetValue<string>('email', Email) then Email := '';
// Validation basique
if (not Nom.Trim.IsEmpty) and
(not Prenom.Trim.IsEmpty) and
(not Email.Trim.IsEmpty) then
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := DMData.Connection;
Query.SQL.Text :=
'INSERT INTO clients (nom, prenom, email, telephone) ' +
'VALUES (:nom, :prenom, :email, :telephone)';
Query.ParamByName('nom').AsString := Nom;
Query.ParamByName('prenom').AsString := Prenom;
Query.ParamByName('email').AsString := Email;
// GetValue<T>(path, default) ne lève pas d'exception si la clé
// est absente : pratique pour les champs optionnels comme le téléphone.
Query.ParamByName('telephone').AsString := Body.GetValue<string>('telephone', '');
Query.ExecSQL;
// Récupérer l'ID du nouveau client — syntaxe MySQL/MariaDB ;
// pour les variantes par SGBD, voir le fichier 23.3.
Query.SQL.Text := 'SELECT LAST_INSERT_ID() as id';
Query.Open;
NewID := Query.FieldByName('id').AsInteger;
Response := TJSONObject.Create;
try
Response.AddPair('success', TJSONBool.Create(True));
Response.AddPair('id', TJSONNumber.Create(NewID));
Response.AddPair('message', 'Client créé avec succès');
// 💡 201 Created + header Location → bonne pratique REST (HATEOAS).
Res.RawWebResponse.SetCustomHeader('Location',
Format('/api/clients/%d', [NewID]));
Res.Status(201).Send<TJSONObject>(Response);
finally
// Response sera libéré automatiquement
end;
finally
Query.Free;
end;
end
else
begin
Response := TJSONObject.Create;
Response.AddPair('success', TJSONBool.Create(False));
Response.AddPair('message', 'Données invalides');
Res.Status(400).Send<TJSONObject>(Response);
end;
end;
// Route PUT /api/clients/:id
procedure UpdateClient(Req: THorseRequest; Res: THorseResponse; Next: TProc);
var
Query: TFDQuery;
Body: TJSONObject;
Response: TJSONObject;
ClientID: Integer;
Nom, Prenom, Email: string;
begin
if not TryStrToInt(Req.Params['id'], ClientID) then
begin
Res.Status(400).Send('ID invalide');
Exit;
end;
Body := Req.Body<TJSONObject>;
// ⚠️ Protéger contre Body=nil et champs manquants — cohérent avec CreateClient.
if not Assigned(Body) or
not Body.TryGetValue<string>('nom', Nom) or
not Body.TryGetValue<string>('prenom', Prenom) or
not Body.TryGetValue<string>('email', Email) then
begin
Res.Status(400).Send('Champs requis : nom, prenom, email');
Exit;
end;
Query := TFDQuery.Create(nil);
try
Query.Connection := DMData.Connection;
Query.SQL.Text :=
'UPDATE clients SET nom = :nom, prenom = :prenom, ' +
'email = :email, telephone = :telephone WHERE id = :id';
Query.ParamByName('id').AsInteger := ClientID;
Query.ParamByName('nom').AsString := Nom;
Query.ParamByName('prenom').AsString := Prenom;
Query.ParamByName('email').AsString := Email;
Query.ParamByName('telephone').AsString := Body.GetValue<string>('telephone', '');
Query.ExecSQL;
Response := TJSONObject.Create;
try
Response.AddPair('success', TJSONBool.Create(True));
Response.AddPair('message', 'Client modifié avec succès');
Res.Send<TJSONObject>(Response);
finally
// Response sera libéré automatiquement
end;
finally
Query.Free;
end;
end;
// Route DELETE /api/clients/:id
procedure DeleteClient(Req: THorseRequest; Res: THorseResponse; Next: TProc);
var
Query: TFDQuery;
ClientID: Integer;
begin
if not TryStrToInt(Req.Params['id'], ClientID) then
begin
Res.Status(400).Send('ID invalide');
Exit;
end;
Query := TFDQuery.Create(nil);
try
Query.Connection := DMData.Connection;
Query.SQL.Text := 'DELETE FROM clients WHERE id = :id';
Query.ParamByName('id').AsInteger := ClientID;
Query.ExecSQL;
// ⚠️ 204 No Content DOIT être renvoyé SANS corps (RFC 9110).
// Aligné avec le pattern du fichier 23.3 ; le client React du
// fichier intercepte déjà ce cas (cf. fonction deleteClient).
Res.Status(204);
finally
Query.Free;
end;
end;
begin
App := THorse.Create;
// Middlewares
App.Use(Jhonson); // Support JSON
App.Use(CORS); // Support CORS
App.Use(HandleException); // Gestion des erreurs
// Routes API
App.Get('/api/clients', GetClients);
App.Get('/api/clients/:id', GetClient);
App.Post('/api/clients', CreateClient);
App.Put('/api/clients/:id', UpdateClient);
App.Delete('/api/clients/:id', DeleteClient);
App.Listen(9000);
Writeln('API Delphi démarrée sur http://localhost:9000');
Writeln('Endpoints disponibles :');
Writeln(' GET /api/clients');
Writeln(' GET /api/clients/:id');
Writeln(' POST /api/clients');
Writeln(' PUT /api/clients/:id');
Writeln(' DELETE /api/clients/:id');
Writeln('');
Writeln('Appuyez sur Entrée pour arrêter...');
Readln;
end.client-app/
├── public/
│ └── index.html
├── src/
│ ├── components/
│ │ ├── ClientsList.js
│ │ ├── ClientForm.js
│ │ └── ClientDetails.js
│ ├── services/
│ │ └── api.js
│ ├── App.js
│ └── index.js
└── package.json
// src/services/api.js
const API_BASE_URL = 'http://localhost:9000/api';
// Configuration de base pour fetch
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
};
// Récupérer tous les clients
export async function getClients() {
try {
const response = await fetch(`${API_BASE_URL}/clients`, defaultOptions);
if (!response.ok) {
throw new Error('Erreur lors de la récupération des clients');
}
return await response.json();
} catch (error) {
console.error('Erreur:', error);
throw error;
}
}
// Récupérer un client par ID
export async function getClient(id) {
try {
const response = await fetch(`${API_BASE_URL}/clients/${id}`, defaultOptions);
if (!response.ok) {
throw new Error('Client non trouvé');
}
return await response.json();
} catch (error) {
console.error('Erreur:', error);
throw error;
}
}
// Créer un nouveau client
export async function createClient(clientData) {
try {
const response = await fetch(`${API_BASE_URL}/clients`, {
...defaultOptions,
method: 'POST',
body: JSON.stringify(clientData),
});
if (!response.ok) {
throw new Error('Erreur lors de la création du client');
}
return await response.json();
} catch (error) {
console.error('Erreur:', error);
throw error;
}
}
// Modifier un client
export async function updateClient(id, clientData) {
try {
const response = await fetch(`${API_BASE_URL}/clients/${id}`, {
...defaultOptions,
method: 'PUT',
body: JSON.stringify(clientData),
});
if (!response.ok) {
throw new Error('Erreur lors de la modification du client');
}
return await response.json();
} catch (error) {
console.error('Erreur:', error);
throw error;
}
}
// Supprimer un client
export async function deleteClient(id) {
try {
const response = await fetch(`${API_BASE_URL}/clients/${id}`, {
...defaultOptions,
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Erreur lors de la suppression du client');
}
// ⚠️ Une réponse 204 No Content n'a PAS de corps : appeler
// response.json() lèverait `SyntaxError: Unexpected end of JSON input`.
// On retourne juste true en cas de succès.
if (response.status === 204) {
return true;
}
return await response.json();
} catch (error) {
console.error('Erreur:', error);
throw error;
}
}// src/components/ClientsList.js
import React, { useState, useEffect } from 'react';
import { getClients, deleteClient } from '../services/api';
function ClientsList({ onEdit }) {
const [clients, setClients] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Charger les clients au montage du composant
useEffect(() => {
loadClients();
}, []);
async function loadClients() {
try {
setLoading(true);
const data = await getClients();
setClients(data);
setError(null);
} catch (err) {
setError('Impossible de charger les clients');
console.error(err);
} finally {
setLoading(false);
}
}
async function handleDelete(id) {
if (window.confirm('Êtes-vous sûr de vouloir supprimer ce client ?')) {
try {
await deleteClient(id);
// Recharger la liste après suppression
loadClients();
} catch (err) {
alert('Erreur lors de la suppression');
console.error(err);
}
}
}
if (loading) {
return <div className="loading">Chargement des clients...</div>;
}
if (error) {
return <div className="error">{error}</div>;
}
return (
<div className="clients-list">
<h2>Liste des clients ({clients.length})</h2>
{clients.length === 0 ? (
<p>Aucun client trouvé.</p>
) : (
<table>
<thead>
<tr>
<th>Nom</th>
<th>Prénom</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{clients.map(client => (
<tr key={client.id}>
<td>{client.nom}</td>
<td>{client.prenom}</td>
<td>{client.email}</td>
<td>
<button onClick={() => onEdit(client)}>
✏️ Modifier
</button>
<button onClick={() => handleDelete(client.id)}>
🗑️ Supprimer
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
export default ClientsList;// src/components/ClientForm.js
import React, { useState, useEffect } from 'react';
import { createClient, updateClient } from '../services/api';
function ClientForm({ client, onSave, onCancel }) {
const [formData, setFormData] = useState({
nom: '',
prenom: '',
email: '',
telephone: '',
});
const [errors, setErrors] = useState({});
const [saving, setSaving] = useState(false);
// Pré-remplir le formulaire si on modifie un client existant
useEffect(() => {
if (client) {
setFormData({
nom: client.nom || '',
prenom: client.prenom || '',
email: client.email || '',
telephone: client.telephone || '',
});
}
}, [client]);
function handleChange(e) {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Effacer l'erreur du champ modifié
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: null
}));
}
}
function validate() {
const newErrors = {};
if (!formData.nom.trim()) {
newErrors.nom = 'Le nom est obligatoire';
}
if (!formData.prenom.trim()) {
newErrors.prenom = 'Le prénom est obligatoire';
}
if (!formData.email.trim()) {
newErrors.email = 'L\'email est obligatoire';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email invalide';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
async function handleSubmit(e) {
e.preventDefault();
if (!validate()) {
return;
}
try {
setSaving(true);
if (client) {
// Modification
await updateClient(client.id, formData);
} else {
// Création
await createClient(formData);
}
onSave();
} catch (err) {
alert('Erreur lors de l\'enregistrement');
console.error(err);
} finally {
setSaving(false);
}
}
return (
<div className="client-form">
<h2>{client ? 'Modifier le client' : 'Nouveau client'}</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="nom">Nom *</label>
<input
type="text"
id="nom"
name="nom"
value={formData.nom}
onChange={handleChange}
className={errors.nom ? 'error' : ''}
/>
{errors.nom && <span className="error-message">{errors.nom}</span>}
</div>
<div className="form-group">
<label htmlFor="prenom">Prénom *</label>
<input
type="text"
id="prenom"
name="prenom"
value={formData.prenom}
onChange={handleChange}
className={errors.prenom ? 'error' : ''}
/>
{errors.prenom && <span className="error-message">{errors.prenom}</span>}
</div>
<div className="form-group">
<label htmlFor="email">Email *</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label htmlFor="telephone">Téléphone</label>
<input
type="tel"
id="telephone"
name="telephone"
value={formData.telephone}
onChange={handleChange}
/>
</div>
<div className="form-actions">
<button type="submit" disabled={saving}>
{saving ? 'Enregistrement...' : 'Enregistrer'}
</button>
<button type="button" onClick={onCancel}>
Annuler
</button>
</div>
</form>
</div>
);
}
export default ClientForm;// src/App.js
import React, { useState } from 'react';
import ClientsList from './components/ClientsList';
import ClientForm from './components/ClientForm';
import './App.css';
function App() {
const [view, setView] = useState('list'); // 'list' ou 'form'
const [selectedClient, setSelectedClient] = useState(null);
function handleNewClient() {
setSelectedClient(null);
setView('form');
}
function handleEditClient(client) {
setSelectedClient(client);
setView('form');
}
function handleSaveComplete() {
setView('list');
setSelectedClient(null);
}
function handleCancel() {
setView('list');
setSelectedClient(null);
}
return (
<div className="App">
<header>
<h1>Gestion des Clients</h1>
{view === 'list' && (
<button onClick={handleNewClient}>
➕ Nouveau client
</button>
)}
</header>
<main>
{view === 'list' ? (
<ClientsList onEdit={handleEditClient} />
) : (
<ClientForm
client={selectedClient}
onSave={handleSaveComplete}
onCancel={handleCancel}
/>
)}
</main>
<footer>
<p>Application React + API Delphi</p>
</footer>
</div>
);
}
export default App;uses
Horse, Horse.JWT, JOSE.Core.JWT, JOSE.Core.Builder, System.DateUtils;
procedure Login(Req: THorseRequest; Res: THorseResponse; Next: TProc);
var
Body: TJSONObject;
Username, Password: string;
JWT: TJWT;
Token: string;
Response: TJSONObject;
begin
Body := Req.Body<TJSONObject>;
// ⚠️ Protéger contre Body=nil ou champs manquants — sinon GetValue
// lève une AccessViolation au lieu de retourner 400.
if not Assigned(Body) or
not Body.TryGetValue<string>('username', Username) or
not Body.TryGetValue<string>('password', Password) then
begin
Response := TJSONObject.Create;
Response.AddPair('success', TJSONBool.Create(False));
Response.AddPair('message', 'username et password requis');
Res.Status(400).Send<TJSONObject>(Response);
Exit;
end;
// ⚠️ Les 5 règles de sécurité JWT (clé secrète, longueur de clé,
// expiration, contenu non sensible, durée courte) sont détaillées
// dans la section 23.3. Le nom `SECRET_KEY_CHANGE_ME` ci-dessous
// est volontairement explicite : en production, charger la clé
// depuis une variable d'environnement, pas depuis le source.
// Vérifier les identifiants (à implémenter selon votre logique)
if VerifyCredentials(Username, Password) then
begin
JWT := TJWT.Create;
try
JWT.Claims.Subject := Username;
JWT.Claims.Expiration := IncHour(Now, 1); // Expire dans 1 h (durée courte recommandée)
JWT.Claims.SetClaimOfType<string>('role', GetUserRole(Username));
Token := TJOSE.SHA256CompactToken('SECRET_KEY_CHANGE_ME', JWT);
Response := TJSONObject.Create;
try
Response.AddPair('success', TJSONBool.Create(True));
Response.AddPair('token', Token);
Response.AddPair('username', Username);
Response.AddPair('expiresIn', '3600'); // 1 h en secondes
Res.Send<TJSONObject>(Response);
finally
// Response sera libéré automatiquement
end;
finally
JWT.Free;
end;
end
else
begin
Response := TJSONObject.Create;
Response.AddPair('success', TJSONBool.Create(False));
Response.AddPair('message', 'Identifiants invalides');
Res.Status(401).Send<TJSONObject>(Response);
end;
end;
// Middleware d'authentification
procedure AuthMiddleware(Req: THorseRequest; Res: THorseResponse; Next: TProc);
var
Token: string;
JWT: TJWT;
begin
Token := Req.Headers['Authorization'];
if Token.IsEmpty then
begin
Res.Status(401).Send('Token manquant');
Exit;
end;
// Retirer "Bearer " du token (IgnoreCase pour tolérer "bearer" en
// minuscules — certains clients ne respectent pas la casse RFC 6750).
if Token.StartsWith('Bearer ', True) then
Token := Token.Substring(7);
try
JWT := TJOSE.Verify('SECRET_KEY_CHANGE_ME', Token);
try
// Token valide, continuer
Next;
finally
JWT.Free;
end;
except
Res.Status(401).Send('Token invalide ou expiré');
end;
end;
// Configuration des routes
begin
// Route de login (publique)
THorse.Post('/api/login', Login);
// Routes protégées
THorse.AddCallback(AuthMiddleware)
.Get('/api/clients', GetClients)
.Post('/api/clients', CreateClient);
end;🚨 Avertissement sécurité — Où stocker le JWT ?
Le choix du stockage du token côté client est un compromis sécurité :
| Stockage | Avantages | Inconvénients |
|---|---|---|
| localStorage | Simple, survit aux fermetures d'onglets | ❌ Vulnérable au XSS — tout script JS peut le lire |
| sessionStorage | Effacé à la fermeture de l'onglet | ❌ Toujours vulnérable au XSS |
| Cookie HttpOnly + Secure + SameSite | ✅ Inaccessible à JS (anti-XSS), envoyé automatiquement | Nécessite gestion côté serveur ; vulnérable au CSRF (à atténuer avec SameSite) |
| Memory (variable JS) | Aucun stockage persistant | Perdu au refresh de page (nécessite refresh token) |
Recommandation 2026 : pour les applications sensibles, stocker le
token (ou un refresh token) dans un cookie HttpOnly + Secure + SameSite=Strict
servi par votre backend Delphi. L'exemple ci-dessous utilise
localStorage pour simplifier l'apprentissage, mais ce n'est PAS
le choix recommandé en production.
// src/services/auth.js
// ⚠️ DÉMO PÉDAGOGIQUE — voir l'avertissement ci-dessus avant
// de déployer en production.
const TOKEN_KEY = 'auth_token';
export function saveToken(token) {
localStorage.setItem(TOKEN_KEY, token);
}
export function getToken() {
return localStorage.getItem(TOKEN_KEY);
}
export function removeToken() {
localStorage.removeItem(TOKEN_KEY);
}
export function isAuthenticated() {
return !!getToken();
}
// Service API modifié avec authentification
// src/services/api.js
import { getToken } from './auth';
function getHeaders() {
const headers = {
'Content-Type': 'application/json',
};
const token = getToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
export async function login(username, password) {
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error('Échec de la connexion');
}
return await response.json();
}
export async function getClients() {
const response = await fetch(`${API_BASE_URL}/clients`, {
headers: getHeaders(),
});
if (response.status === 401) {
// Token expiré ou invalide
throw new Error('NON_AUTHENTIFIE');
}
if (!response.ok) {
throw new Error('Erreur lors de la récupération des clients');
}
return await response.json();
}
// Composant Login
// src/components/Login.js
import React, { useState } from 'react';
import { login } from '../services/api';
import { saveToken } from '../services/auth';
function Login({ onLoginSuccess }) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
try {
const response = await login(username, password);
if (response.success) {
saveToken(response.token);
onLoginSuccess(response.username);
} else {
setError(response.message || 'Échec de la connexion');
}
} catch (err) {
setError('Erreur de connexion');
console.error(err);
} finally {
setLoading(false);
}
}
return (
<div className="login-container">
<h2>Connexion</h2>
<form onSubmit={handleSubmit}>
{error && <div className="error-message" role="alert">{error}</div>}
<div className="form-group">
{/* htmlFor relie le label à l'input (accessibilité +
cliquer sur le label focus l'input).
autoComplete aide les gestionnaires de mots de passe
et déclenche le remplissage automatique du navigateur. */}
<label htmlFor="login-username">Nom d'utilisateur</label>
<input
id="login-username"
name="username"
type="text"
autoComplete="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="login-password">Mot de passe</label>
<input
id="login-password"
name="password"
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Connexion...' : 'Se connecter'}
</button>
</form>
</div>
);
}
export default Login;// src/services/api.js
async function handleResponse(response) {
if (!response.ok) {
// Tenter de lire le message d'erreur du serveur
let errorMessage = 'Une erreur est survenue';
try {
const errorData = await response.json();
errorMessage = errorData.message || errorMessage;
} catch {
// Le serveur n'a pas renvoyé de JSON
}
const error = new Error(errorMessage);
error.status = response.status;
throw error;
}
return response.json();
}
export async function getClients() {
try {
const response = await fetch(`${API_BASE_URL}/clients`, {
headers: getHeaders(),
});
return await handleResponse(response);
} catch (error) {
if (error.status === 401) {
// Rediriger vers login
window.location.href = '/login';
}
throw error;
}
}
// Hook personnalisé pour gérer les erreurs
// src/hooks/useApiError.js
import { useState } from 'react';
export function useApiError() {
const [error, setError] = useState(null);
function handleError(err) {
console.error('Erreur API:', err);
let message = 'Une erreur est survenue';
if (err.status === 401) {
message = 'Vous devez vous connecter';
} else if (err.status === 403) {
message = 'Accès refusé';
} else if (err.status === 404) {
message = 'Ressource non trouvée';
} else if (err.status >= 500) {
message = 'Erreur serveur. Veuillez réessayer plus tard.';
} else if (err.message) {
message = err.message;
}
setError(message);
}
function clearError() {
setError(null);
}
return { error, handleError, clearError };
}
// Utilisation
function ClientsList() {
const [clients, setClients] = useState([]);
const { error, handleError, clearError } = useApiError();
async function loadClients() {
try {
clearError();
const data = await getClients();
setClients(data);
} catch (err) {
handleError(err);
}
}
return (
<div>
{error && <div className="error-banner">{error}</div>}
{/* ... reste du composant */}
</div>
);
}Backend Delphi :
- Serveur dédié (VPS, cloud)
- URL :
https://api.monapp.com - Port 9000 ou 443 (HTTPS)
Frontend React :
- Hébergement statique (Netlify, Vercel, GitHub Pages)
- URL :
https://app.monapp.com - Appels API vers le backend
Serveur unique Delphi servant :
- API REST :
/api/* - Fichiers statiques React :
/*
// Servir les fichiers statiques React.
// 🚨 SÉCURITÉ : Cette fonction DOIT se protéger contre la traversée de
// répertoire. Une requête comme GET /../../../etc/passwd ne doit
// JAMAIS pouvoir lire un fichier en dehors du dossier `public`.
// Le pattern : résoudre le chemin absolu, puis vérifier qu'il
// commence bien par le dossier racine attendu.
procedure ServeStaticFiles(Req: THorseRequest; Res: THorseResponse; Next: TProc);
var
RawPath, RootDir, FullPath: string;
begin
RawPath := Req.PathInfo;
if (RawPath = '') or (RawPath = '/') then
RawPath := '/index.html';
// Retirer le '/' initial pour que TPath.Combine ne le traite pas
// comme un chemin absolu (sinon TPath.Combine ignore 'public').
if RawPath.StartsWith('/') then
RawPath := RawPath.Substring(1);
RootDir := TPath.GetFullPath('public');
FullPath := TPath.GetFullPath(TPath.Combine(RootDir, RawPath));
// ⚠️ Garde-fou anti path-traversal : le chemin résolu DOIT rester
// sous RootDir. Sans ce test, un attaquant peut sortir du sandbox.
if not FullPath.StartsWith(RootDir + PathDelim) then
begin
Res.Status(403).Send('Accès refusé');
Exit;
end;
if TFile.Exists(FullPath) then
Res.SendFile(FullPath)
else
Next; // Passer au handler suivant
end;
begin
// Routes API
THorse.Get('/api/clients', GetClients);
// Fichiers statiques (après les routes API)
THorse.Get('/*', ServeStaticFiles);
THorse.Listen(9000);
end;Create React App (legacy, déprécié en 2023 mais encore très répandu) :
# .env
REACT_APP_API_URL=http://localhost:9000/api
const API_BASE_URL = process.env.REACT_APP_API_URL
|| 'http://localhost:9000/api';Vite (recommandé pour les nouveaux projets) :
# .env
VITE_API_URL=http://localhost:9000/api
const API_BASE_URL = import.meta.env.VITE_API_URL
|| 'http://localhost:9000/api';💡 Depuis 2023, l'équipe React recommande explicitement d'utiliser
Vite, Next.js, Remix… plutôt que Create React App pour les nouveaux
projets. CRA n'est plus maintenu.
function ClientsList() {
const [loading, setLoading] = useState(false);
const [clients, setClients] = useState([]);
async function loadClients() {
setLoading(true);
try {
const data = await getClients();
setClients(data);
} finally {
setLoading(false); // Toujours arrêter le loading
}
}
if (loading) {
return <div className="spinner">Chargement...</div>;
}
return <div>{/* contenu */}</div>;
}import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Utilisation
function SearchClients() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearch) {
// Effectuer la recherche seulement après 500ms d'inactivité
searchClients(debouncedSearch);
}
}, [debouncedSearch]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Rechercher..."
/>
);
}async function handleDelete(id) {
// Supprimer immédiatement de l'UI
setClients(prev => prev.filter(c => c.id !== id));
try {
await deleteClient(id);
} catch (error) {
// En cas d'erreur, restaurer
loadClients();
alert('Erreur lors de la suppression');
}
}L'intégration de Delphi avec des frameworks JavaScript modernes offre le meilleur des deux mondes :
✅ Backend solide avec Delphi - Performance, fiabilité, accès données
✅ Frontend moderne avec React/Vue/Angular - UX exceptionnelle
✅ Architecture découplée - Évolution indépendante
✅ Scalabilité - Chaque partie peut être optimisée séparément
✅ Écosystème riche - Profiter des deux communautés
Cette approche est idéale pour :
- Moderniser des applications Delphi existantes
- Créer de nouvelles applications avec interface web moderne
- Permettre à des développeurs JavaScript de travailler sur le frontend
- Conserver l'expertise Delphi pour le backend critique
Dans la section suivante, nous explorerons les Progressive Web Apps (PWA), qui permettent de transformer vos applications web en applications installables fonctionnant hors ligne.