Architecture REST
| Méthode | URL | Action | Code succÚs |
| GET | /api/v1/tasks | Lister (paginé) | 200 |
| POST | /api/v1/tasks | Créer | 201 |
| GET | /api/v1/tasks/{id} | Détail | 200 |
| PUT/PATCH | /api/v1/tasks/{id} | Modifier | 200 |
| DELETE | /api/v1/tasks/{id} | Supprimer | 204 |
// routes/api.php â versioning
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->group(function() {
// Public
Route::post('/auth/register', [AuthController::class, 'register']);
Route::post('/auth/login', [AuthController::class, 'login']);
// Protégé Sanctum
Route::middleware('auth:sanctum')->group(function() {
Route::get('/auth/me', [AuthController::class, 'me']);
Route::post('/auth/logout', [AuthController::class, 'logout']);
Route::apiResource('tasks', TaskController::class);
Route::apiResource('projects', ProjectController::class);
// Nested resource
Route::apiResource('projects.tasks', ProjectTaskController::class)
->shallow();
});
});
API Resources
php artisan make:resource TaskResource
php artisan make:resource TaskCollection # collection avec méta
// app/Http/Resources/TaskResource.php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
public function toArray(Request $request): array {
return [
'id' => $this->id,
'title' => $this->title,
'status' => $this->status,
'priority' => $this->priority,
'is_overdue' => $this->when($this->isOverdue(), true),
'created_at' => $this->created_at->toIso8601String(),
// Relation incluse seulement si eager-loadée
'project' => new ProjectResource($this->whenLoaded('project')),
'assignee' => new UserResource($this->whenLoaded('assignee')),
'comments_count' => $this->when(
$this->relationLoaded('comments'),
fn() => $this->comments->count()
),
];
}
}
// Collection avec méta custom
class TaskCollection extends ResourceCollection
{
public function toArray(Request $request): array {
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'last_page' => $this->lastPage(),
'filters' => $request->only(['status', 'priority']),
],
];
}
}
// Utilisation dans le controller
public function index(Request $request): AnonymousResourceCollection {
$tasks = Task::with(['project', 'assignee'])
->when($request->status, fn($q) => $q->where('status', $request->status))
->paginate($request->per_page ?? 15);
return TaskResource::collection($tasks);
}
public function store(StoreTaskRequest $request): TaskResource {
$task = Task::create($request->validated());
return (new TaskResource($task))->response()->setStatusCode(201);
}
public function show(Task $task): TaskResource {
$task->load('project', 'assignee', 'comments');
return new TaskResource($task);
}
Form Requests
php artisan make:request StoreTaskRequest
php artisan make:request UpdateTaskRequest
// app/Http/Requests/StoreTaskRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class StoreTaskRequest extends FormRequest
{
public function authorize(): bool {
// Vérifier que l'utilisateur peut créer une tùche dans ce projet
$project = $this->route('project');
return $project === null || $this->user()->can('update', $project);
}
public function rules(): array {
return [
'title' => 'required|string|max:255',
'body' => 'nullable|string',
'status' => 'in:todo,in_progress,done',
'priority' => 'in:low,medium,high',
'due_date' => 'nullable|date|after:today',
'assignee_id' => 'nullable|exists:users,id',
'tags' => 'array',
'tags.*' => 'string|max:50',
];
}
public function messages(): array {
return [
'title.required' => 'Le titre est obligatoire.',
'title.max' => 'Le titre ne doit pas dépasser 255 caractÚres.',
'due_date.after' => 'La date d\'Ă©chĂ©ance doit ĂȘtre dans le futur.',
'assignee_id.exists' => 'L\'utilisateur assigné n\'existe pas.',
];
}
public function attributes(): array {
return [
'title' => 'titre',
'body' => 'description',
'due_date' => 'date d\'échéance',
'assignee_id' => 'assigné',
];
}
// Personnaliser la réponse 422 (pour une API)
protected function failedValidation(Validator $validator): void {
throw new HttpResponseException(
response()->json([
'message' => 'Données invalides.',
'errors' => $validator->errors(),
], 422)
);
}
}
Sanctum â Token abilities
// Créer un token avec abilities (scopes)
$token = $user->createToken('mobile-app', ['tasks:read', 'tasks:write']);
// Vérifier une ability
$request->user()->tokenCan('tasks:write');
// Définir les abilities dans les routes
Route::middleware(['auth:sanctum', 'abilities:tasks:read'])->group(function() {
Route::get('/tasks', [TaskController::class, 'index']);
});
Route::middleware(['auth:sanctum', 'ability:tasks:write'])->group(function() {
Route::post('/tasks', [TaskController::class, 'store']);
});
// Révoquer
$user->currentAccessToken()->delete(); // token courant
$user->tokens()->where('name', 'mobile-app')->delete(); // par nom
$user->tokens()->delete(); // tous
Versioning API
// bootstrap/app.php
->withRouting(
api: __DIR__.'/../routes/api.php', // /api/v1
health: '/up',
)
// routes/api.php
Route::prefix('v1')->name('api.v1.')->group(base_path('routes/api_v1.php'));
Route::prefix('v2')->name('api.v2.')->group(base_path('routes/api_v2.php'));
Gestion d'erreurs JSON
// bootstrap/app.php â centraliser les erreurs
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (\Illuminate\Auth\AuthenticationException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json(['message' => 'Non authentifié.'], 401);
}
});
$exceptions->render(function (\Illuminate\Auth\Access\AuthorizationException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json(['message' => 'AccÚs refusé.'], 403);
}
});
$exceptions->render(function (\Illuminate\Database\Eloquent\ModelNotFoundException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'message' => 'Ressource introuvable.',
'model' => class_basename($e->getModel()),
], 404);
}
});
$exceptions->render(function (\Illuminate\Validation\ValidationException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'message' => 'Données invalides.',
'errors' => $e->errors(),
], 422);
}
});
})
Documentation API
# Option 1 : Scramble (plus simple, auto-génération)
composer require dedoc/scramble
# â /docs/api automatiquement
# Option 2 : L5-Swagger (annotations OpenAPI)
composer require darkaonline/l5-swagger
php artisan l5-swagger:generate
// Scramble â configuration dans config/scramble.php
return [
'api_path' => 'api/v1',
'info' => [
'version' => '1.0.0',
'title' => 'TaskManager API',
],
];