Le guide pas-à-pas du back-end du projet fil rouge. Une étape par séance : l'objectif, les étapes, le code corrigé, et comment vérifier. À la fin, tu tiens une vraie API Laravel — et tu la branches sur le front Angular, à la place de json-server.
Atelier API — le back-end de clients, projets et factures : la moitié serveur de l'outil qu'on utilise en interne chez MarQenti. À la fin : une vraie API Laravel avec base de données, relations, CRUD validé, authentification, sécurité, performance et mise en ligne — qui remplace la fausse API (json-server) du cours Angular sans rien changer côté front.
une table, un modèle Eloquent, et les 5 routes REST /api/clients.
rattachés à un client (hasMany), avec un statut.
montant, payé ou non — le CA total calculé côté serveur.
Pendant le cours Angular, le front parlait à json-server (une fausse API qui lit db.json). Ici tu construis la vraie API — mêmes chemins (/clients, /projets, /factures), même forme JSON. Résultat : à la dernière étape, on ne change qu'une URL côté Angular, et json-server disparaît.
Après chaque séance, ouvre l'étape correspondante, fais-la, et vérifie avec la case « Vérification ». Ne saute pas d'étape — chacune construit sur la précédente. Le code est la correction : essaie d'abord, compare ensuite.
Avoir un projet Laravel « atelier-api » qui tourne sur ta machine, avec le mode API activé dès le départ.
php -v (8.2+) et composer -V. Sinon installe-les.routes/api.php + Sanctum), puis lance le serveur.terminalcomposer create-project laravel/laravel atelier-api cd atelier-api php artisan install:api # crée routes/api.php + installe Sanctum php artisan serve # → http://localhost:8000
localhost:8000 affiche la page d'accueil Laravel.routes/api.php existe ; GET /api/user répond 401 sans token — c'est normal, la route est protégée.Comprendre le trajet d'une requête — route → contrôleur → réponse JSON — et la différence entre web.php et api.php.
routes/api.php.terminalphp artisan make:controller ClientController
routes/api.phpuse App\Http\Controllers\ClientController;
Route::get('/clients', [ClientController::class, 'index']);
app/Http/Controllers/ClientController.phppublic function index()
{
return response()->json([
['id' => 1, 'nom' => 'Boulangerie Zitoun'],
]);
}
GET localhost:8000/api/clients renvoie le JSON (statut 200, en-tête Content-Type: application/json).api.php est préfixé par /api et renvoie des données ; web.php renvoie des pages.Exposer les cinq points d'entrée REST d'une ressource proprement, avec les bons verbes HTTP et les bons codes de statut.
--api).apiResource.terminalphp artisan make:controller ClientController --api
routes/api.phpRoute::apiResource('clients', ClientController::class); // → GET /clients · POST /clients · GET /clients/{id} // PUT/PATCH /clients/{id} · DELETE /clients/{id}
les codes de statut qui comptentreturn response()->json($client, 201); // créé return response()->noContent(); // 204, après delete abort(404); // introuvable
php artisan route:list montre les cinq routes clients.201 ; un GET sur un id inconnu renvoie 404.Comprendre la différence entre une vue Blade (HTML rendu serveur) et une API (JSON pour une SPA) — et renvoyer les clients au même format que json-server, pour qu'Angular ne voie aucune différence.
web.php pour une page d'accueil simple, api.php pour les données.db.json.web.php vs api.php// web.php — une VUE (HTML rendu serveur, Blade) Route::get('/', fn () => view('welcome')); // api.php — des DONNÉES (JSON pour la SPA Angular) Route::get('/clients', fn () => [ ['id' => 1, 'nom' => 'Boulangerie Zitoun', 'email' => 'z@ex.tn', 'telephone' => '22 000 111'], ]); // Laravel sérialise le tableau en JSON
GET /api/clients renvoie exactement la forme attendue par Angular (mêmes clés que db.json).Une base SQLite branchée, et un schéma versionné par des migrations pour clients, projets et factures.
.env (le plus simple pour démarrer) et crée le fichier..env — la connexionDB_CONNECTION=sqlite # commente DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD
terminaltouch database/database.sqlite
php artisan make:migration create_clients_table
php artisan make:migration create_projets_table
php artisan make:migration create_factures_table
database/migrations/..._create_clients_table.phpSchema::create('clients', function (Blueprint $table) {
$table->id();
$table->string('nom');
$table->string('email');
$table->string('telephone');
$table->timestamps();
});
php artisan migrate crée les tables sans erreur.database/database.sqlite existe ; les trois tables sont là.Un modèle par table, et les relations client → projets / factures — pour écrire du PHP au lieu du SQL.
hasMany côté client, belongsTo côté enfant.$fillable).terminalphp artisan make:model Projet php artisan make:model Facture # (Client existe déjà)
app/Models/Client.phpclass Client extends Model { protected $fillable = ['nom', 'email', 'telephone']; public function projets() { return $this->hasMany(Projet::class); } public function factures() { return $this->hasMany(Facture::class); } }
app/Models/Projet.php + la migration// modèle protected $fillable = ['client_id', 'titre', 'statut']; public function client() { return $this->belongsTo(Client::class); } // dans la migration projets ET factures : $table->foreignId('client_id')->constrained()->cascadeOnDelete();
php artisan tinker : Client::create([...]) puis $client->projets renvoie une collection.$facture->client.Remplir la base de données de démo réalistes en une seule commande — pour développer, démontrer et tester.
migrate:fresh --seed.terminalphp artisan make:factory ClientFactory --model=Client
database/factories/ClientFactory.phppublic function definition(): array
{
return [
'nom' => fake()->company(),
'email' => fake()->safeEmail(),
'telephone' => fake()->numerify('## ### ###'),
];
}
database/seeders/DatabaseSeeder.phpClient::factory(20) ->has(Projet::factory()->count(3)) ->has(Facture::factory()->count(5)) ->create();
terminalphp artisan migrate:fresh --seed # remet à zéro + remplit
Client::count() renvoie 20 ; chacun a des projets et des factures.Les cinq opérations branchées sur la vraie base, avec une entrée validée (Form Request) et une sortie JSON façonnée (API Resource).
Client.terminalphp artisan make:request StoreClientRequest
php artisan make:resource ClientResource
app/Http/Requests/StoreClientRequest.phppublic function rules(): array
{
return [
'nom' => ['required', 'string', 'max:255'],
'email' => ['required', 'email'],
'telephone' => ['required', 'string'],
];
}
ClientController.php — créer & listerpublic function store(StoreClientRequest $request) { $client = Client::create($request->validated()); return new ClientResource($client); // 201 + JSON propre } public function index() { return ClientResource::collection(Client::all()); }
422 avec les messages d'erreur.Une connexion qui renvoie un token, et des routes protégées — exactement le token que l'intercepteur Angular envoie.
AuthController.login vérifie les identifiants et renvoie un token.auth:sanctum.app/Http/Controllers/AuthController.phppublic function login(Request $request) { $request->validate(['email' => 'required|email', 'password' => 'required']); $user = User::where('email', $request->email)->first(); if (! $user || ! Hash::check($request->password, $user->password)) { return response()->json(['message' => 'Identifiants invalides'], 401); } return ['token' => $user->createToken('atelier')->plainTextToken]; }
routes/api.php — protéger la ressourceRoute::post('/login', [AuthController::class, 'login']); Route::middleware('auth:sanctum')->group(function () { Route::apiResource('clients', ClientController::class); Route::get('/user', fn (Request $r) => $r->user()); });
POST /api/login avec de bons identifiants renvoie un token ; sinon 401./api/clients sans l'en-tête Authorization: Bearer … renvoie 401 ; avec, ça passe.Laisser passer le front Angular (CORS) et limiter les abus — un plafond strict sur le login pour couper le brute-force.
/login.429 Too Many Requests.terminal — publier la config CORSphp artisan config:publish cors
config/cors.php — autoriser Angular'paths' => ['api/*', 'login'], 'allowed_methods' => ['*'], 'allowed_origins' => [ 'http://localhost:4200', // le dev Angular 'https://atelier.tn', // le front en prod ],
routes/api.php — limiter le loginRoute::post('/login', [AuthController::class, 'login']) ->middleware('throttle:5,1'); // 5 essais/min → 429 ensuite
localhost:4200), les requêtes passent — aucune erreur CORS dans la console./login renvoie 429 après cinq essais dans la minute.Une API qui reste rapide en grandissant : tuer le N+1 avec l'eager loading, paginer, et indexer les clés étrangères.
index pour tuer le N+1.client_id sur projets et factures.ClientController@index — 2 requêtes, paginépublic function index() { return ClientResource::collection( Client::with(['projets', 'factures'])->paginate(15) ); }
migration — indexer la clé étrangèreSchema::table('projets', function (Blueprint $table) { $table->index('client_id'); // recherches par client → instantanées });
data, meta, links) — ce que le front sait déjà lire.Mettre l'API en ligne, puis pointer le front Angular dessus et retirer json-server. Le produit full-stack, livré.
.env de production, APP_ENV=production, APP_DEBUG=false.environment.prod.ts → l'URL de la vraie API.sur le serveur de prodphp artisan migrate --force # migre sans confirmation interactive php artisan config:cache php artisan route:cache
ANGULAR — src/environments/environment.prod.tsexport const environment = { production: true, apiUrl: 'https://api.atelier.tn/api', // la VRAIE API Laravel }; // json-server : supprimé. Le contrat JSON n'a pas changé.
apiUrl.Authorization → l'API protège ses routes.