Chapitre 945 min1 exercice
Laravel et Next.js ensemble
C'est la configuration de nos projets : l'API Laravel du chapitre 7 et le front Next.js du chapitre 8, dans un seul compose.yaml. Une seule notion nouvelle, mais elle piège tout le monde.
L'organisation du dépôt
projet/
├── compose.yaml
├── .env.example UID, GID, mots de passe factices
├── docker/
│ ├── nginx/default.conf
│ └── php/Dockerfile
├── api/ Laravel
│ └── .env.example
└── front/ Next.js
├── Dockerfile.dev
└── DockerfileDeux adresses pour la même API
Avec Next.js, un appel à l'API peut partir de deux endroits : du navigateur (composant client) ou du serveur Next.js (Server Component, route handler). Ils ne sont pas au même endroit du réseau, donc ils n'utilisent pas la même adresse. Teste les quatre combinaisons.
On déclare donc deux variables, et on choisit selon l'endroit où le code s'exécute.
export function apiUrl(path: string) {
// Côté serveur : on reste dans le réseau Docker
// Côté navigateur : on passe par le port publié
const base =
typeof window === "undefined"
? process.env.API_URL_INTERNAL
: process.env.NEXT_PUBLIC_API_URL;
return `${base}${path}`;
}Le compose.yaml complet
Rien de nouveau : les services du chapitre 7, plus le service front du chapitre 8.
services:
nginx:
image: nginx:stable-alpine
ports:
- "8000:80"
volumes:
- ./api:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
php:
build:
context: ./docker/php
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
volumes:
- ./api:/var/www/html
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:18
environment:
POSTGRES_DB: laravel
POSTGRES_USER: laravel
POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
volumes:
- db-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U laravel -d laravel"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:8-alpine
front:
build:
context: ./front
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- ./front:/app
- front-modules:/app/node_modules
- front-next:/app/.next
environment:
NEXT_PUBLIC_API_URL: http://localhost:8000
API_URL_INTERNAL: http://nginx
depends_on:
- nginx
volumes:
db-data:
front-modules:
front-next:docker compose up -d --build
docker compose ps
# Front : localhost:3000 API : localhost:8000CORS et Sanctum
Le front (localhost:3000) et l'API (localhost:8000) n'ont pas le même port, donc pas la même origine. Le navigateur bloque les appels si Laravel ne les autorise pas explicitement.
docker compose exec php php artisan install:api # routes/api.php et Sanctum
docker compose exec php php artisan config:publish corsreturn [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'supports_credentials' => true,
];FRONTEND_URL=http://localhost:3000
SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=localhostMission finale
Tout ce que tu as appris, dans un seul exercice. Prends ton temps : c'est exactement ce que tu feras sur un vrai projet.
Une liste d'utilisateurs, de PostgreSQL au navigateur
- Monte l'arborescence de ce chapitre avec un Laravel neuf dans
api/et un Next.js neuf dansfront/. - Démarre les cinq services avec une seule commande.
- Côté Laravel : installe l'API, crée cinq utilisateurs et expose
GET /api/users. - Côté Next.js : affiche la liste dans la page d'accueil avec un Server Component.
- Ajoute un bouton client Recharger qui rappelle l'API depuis le navigateur. Règle CORS si besoin.
Ce que tu dois obtenir
docker compose ps: cinq services,dbenhealthy.localhost:8000/api/usersrenvoie un tableau JSON de cinq utilisateurs.localhost:3000affiche ces utilisateurs dès le chargement (rendu serveur).- Le bouton Recharger refait l'appel depuis le navigateur, sans erreur CORS dans la console.
- Après
docker compose downpuisup -d, les utilisateurs sont toujours là.
Un indice
Avance couche par couche et vérifie chaque étape avant de passer à la suivante :
la base (docker compose ps), puis l'API (curl localhost:8000/api/users), puis le rendu serveur, et enfin l'appel client.
Le Server Component doit utiliser API_URL_INTERNAL, le composant client NEXT_PUBLIC_API_URL.
Voir la solution
docker compose exec php php artisan install:api
docker compose exec php php artisan config:publish cors
docker compose exec php php artisan tinker --execute="App\Models\User::factory(5)->create()"use App\Models\User;
Route::get('/users', fn () => User::select('id', 'name', 'email')->get());import { apiUrl } from "@/lib/api";
import { ReloadUsers } from "./reload-users";
type User = { id: number; name: string; email: string };
export default async function Home() {
const users: User[] = await fetch(apiUrl("/api/users"), { cache: "no-store" }).then((r) => r.json());
return (
<main>
<h1>{users.length} utilisateurs</h1>
<ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
<ReloadUsers />
</main>
);
}"use client";
import { useState } from "react";
import { apiUrl } from "@/lib/api";
export function ReloadUsers() {
const [count, setCount] = useState<number | null>(null);
const reload = async () => {
const users = await fetch(apiUrl("/api/users")).then((r) => r.json());
setCount(users.length);
};
return (
<button onClick={reload}>
Recharger {count !== null && `(${count} reçus par le navigateur)`}
</button>
);
}