[{"data":1,"prerenderedAt":1508},["ShallowReactive",2],{"blog-construire-une-api-restful-avec-laravel":3},{"id":4,"title":5,"body":6,"coverImage":1494,"description":1495,"extension":1496,"meta":1497,"navigation":232,"path":1498,"published":232,"publishedAt":1499,"readTime":581,"seo":1500,"stem":1501,"tags":1502,"__hash__":1507},"blog\u002Fblog\u002Fconstruire-une-api-restful-avec-laravel.md","Construire une API RESTful claire et robuste avec Laravel",{"type":7,"value":8,"toc":1481},"minimark",[9,14,18,21,25,28,61,68,72,79,116,119,166,169,183,187,202,209,285,288,412,415,419,422,753,756,780,783,787,790,793,823,826,879,882,890,900,904,911,1099,1102,1113,1120,1124,1127,1130,1161,1164,1299,1302,1306,1309,1312,1329,1332,1335,1351,1354,1358,1361,1364,1389,1391,1467,1471,1474,1477],[10,11,13],"h2",{"id":12},"introduction","Introduction",[15,16,17],"p",{},"Une bonne API ne se résume pas à quelques routes qui renvoient du JSON. Elle doit être compréhensible, cohérente, sécurisée et facile à maintenir dans le temps. Laravel est particulièrement adapté à cet objectif parce qu'il fournit des outils très solides pour gérer les routes, la validation, l'authentification, la pagination et les erreurs.",[15,19,20],{},"Dans cet article, nous allons construire une base propre pour une API RESTful de blog. L'idée n'est pas seulement de faire fonctionner le code, mais de comprendre pourquoi chaque brique existe et comment elle s'assemble avec les autres.",[10,22,24],{"id":23},"ce-quune-api-restful-doit-faire","Ce qu'une API RESTful doit faire",[15,26,27],{},"Une API RESTful manipule des ressources. Dans un blog, les ressources peuvent être des articles, des utilisateurs ou des commentaires. Chaque ressource est exposée avec des verbes HTTP simples :",[29,30,31,39,45,55],"ul",{},[32,33,34,38],"li",{},[35,36,37],"code",{},"GET"," pour lire",[32,40,41,44],{},[35,42,43],{},"POST"," pour créer",[32,46,47,50,51,54],{},[35,48,49],{},"PUT"," ou ",[35,52,53],{},"PATCH"," pour modifier",[32,56,57,60],{},[35,58,59],{},"DELETE"," pour supprimer",[15,62,63,64,67],{},"Le but est d'avoir une interface prévisible. Quand un développeur voit ",[35,65,66],{},"\u002Fapi\u002Fposts",", il doit immédiatement comprendre qu'il travaille avec la collection des articles.",[10,69,71],{"id":70},"mise-en-place-du-projet","Mise en place du projet",[15,73,74,75,78],{},"Commencez par créer un nouveau projet Laravel puis configurez la base de données dans le fichier ",[35,76,77],{},".env",". À ce stade, l'objectif est simplement d'avoir une application prête à recevoir une API.",[80,81,86],"pre",{"className":82,"code":83,"language":84,"meta":85,"style":85},"language-bash shiki shiki-themes github-light github-dark","composer create-project laravel\u002Flaravel mon-api\ncd mon-api\n","bash","",[35,87,88,107],{"__ignoreMap":85},[89,90,93,97,101,104],"span",{"class":91,"line":92},"line",1,[89,94,96],{"class":95},"sScJk","composer",[89,98,100],{"class":99},"sZZnC"," create-project",[89,102,103],{"class":99}," laravel\u002Flaravel",[89,105,106],{"class":99}," mon-api\n",[89,108,110,114],{"class":91,"line":109},2,[89,111,113],{"class":112},"sj4cs","cd",[89,115,106],{"class":99},[15,117,118],{},"Ensuite, créez le modèle, la migration et le contrôleur de ressource. Cela vous donne une structure de départ propre :",[80,120,122],{"className":82,"code":121,"language":84,"meta":85,"style":85},"php artisan make:model Post -mcr\nphp artisan make:request StorePostRequest\nphp artisan make:resource PostResource\n",[35,123,124,141,153],{"__ignoreMap":85},[89,125,126,129,132,135,138],{"class":91,"line":92},[89,127,128],{"class":95},"php",[89,130,131],{"class":99}," artisan",[89,133,134],{"class":99}," make:model",[89,136,137],{"class":99}," Post",[89,139,140],{"class":112}," -mcr\n",[89,142,143,145,147,150],{"class":91,"line":109},[89,144,128],{"class":95},[89,146,131],{"class":99},[89,148,149],{"class":99}," make:request",[89,151,152],{"class":99}," StorePostRequest\n",[89,154,156,158,160,163],{"class":91,"line":155},3,[89,157,128],{"class":95},[89,159,131],{"class":99},[89,161,162],{"class":99}," make:resource",[89,164,165],{"class":99}," PostResource\n",[15,167,168],{},"Ce trio est très utile :",[29,170,171,177,180],{},[32,172,173,174],{},"le modèle représente la table ",[35,175,176],{},"posts",[32,178,179],{},"la requête de validation centralise les règles",[32,181,182],{},"la ressource contrôle la forme de la réponse JSON",[10,184,186],{"id":185},"structure-des-routes-api","Structure des routes API",[15,188,189,190,193,194,197,198,201],{},"Définissez vos routes dans ",[35,191,192],{},"routes\u002Fapi.php",". Laravel applique automatiquement le préfixe ",[35,195,196],{},"\u002Fapi"," et le middleware ",[35,199,200],{},"api"," à ces routes.",[15,203,204,205,208],{},"Pour une API RESTful, la bonne base est souvent ",[35,206,207],{},"apiResource",", car elle crée les routes standard à votre place et garde la convention claire.",[80,210,213],{"className":211,"code":212,"language":128,"meta":85,"style":85},"language-php shiki shiki-themes github-light github-dark","use App\\Http\\Controllers\\PostController;\n\nRoute::apiResource('users', UserController::class);\nRoute::apiResource('posts', PostController::class);\n",[35,214,215,228,234,262],{"__ignoreMap":85},[89,216,217,221,224],{"class":91,"line":92},[89,218,220],{"class":219},"szBVR","use",[89,222,223],{"class":112}," App\\Http\\Controllers\\PostController",[89,225,227],{"class":226},"sVt8B",";\n",[89,229,230],{"class":91,"line":109},[89,231,233],{"emptyLinePlaceholder":232},true,"\n",[89,235,236,239,242,244,247,250,253,256,259],{"class":91,"line":155},[89,237,238],{"class":112},"Route",[89,240,241],{"class":219},"::",[89,243,207],{"class":95},[89,245,246],{"class":226},"(",[89,248,249],{"class":99},"'users'",[89,251,252],{"class":226},", ",[89,254,255],{"class":112},"UserController",[89,257,258],{"class":219},"::class",[89,260,261],{"class":226},");\n",[89,263,265,267,269,271,273,276,278,281,283],{"class":91,"line":264},4,[89,266,238],{"class":112},[89,268,241],{"class":219},[89,270,207],{"class":95},[89,272,246],{"class":226},[89,274,275],{"class":99},"'posts'",[89,277,252],{"class":226},[89,279,280],{"class":112},"PostController",[89,282,258],{"class":219},[89,284,261],{"class":226},[15,286,287],{},"Si certaines actions doivent être protégées, isolez-les dans un groupe de middleware :",[80,289,291],{"className":211,"code":290,"language":128,"meta":85,"style":85},"Route::middleware('auth:sanctum')->group(function () {\n    Route::post('posts', [PostController::class, 'store']);\n    Route::put('posts\u002F{post}', [PostController::class, 'update']);\n    Route::delete('posts\u002F{post}', [PostController::class, 'destroy']);\n});\n",[35,292,293,324,353,380,406],{"__ignoreMap":85},[89,294,295,297,299,302,304,307,310,313,316,318,321],{"class":91,"line":92},[89,296,238],{"class":112},[89,298,241],{"class":219},[89,300,301],{"class":95},"middleware",[89,303,246],{"class":226},[89,305,306],{"class":99},"'auth:sanctum'",[89,308,309],{"class":226},")",[89,311,312],{"class":219},"->",[89,314,315],{"class":95},"group",[89,317,246],{"class":226},[89,319,320],{"class":219},"function",[89,322,323],{"class":226}," () {\n",[89,325,326,329,331,334,336,338,341,343,345,347,350],{"class":91,"line":109},[89,327,328],{"class":112},"    Route",[89,330,241],{"class":219},[89,332,333],{"class":95},"post",[89,335,246],{"class":226},[89,337,275],{"class":99},[89,339,340],{"class":226},", [",[89,342,280],{"class":112},[89,344,258],{"class":219},[89,346,252],{"class":226},[89,348,349],{"class":99},"'store'",[89,351,352],{"class":226},"]);\n",[89,354,355,357,359,362,364,367,369,371,373,375,378],{"class":91,"line":155},[89,356,328],{"class":112},[89,358,241],{"class":219},[89,360,361],{"class":95},"put",[89,363,246],{"class":226},[89,365,366],{"class":99},"'posts\u002F{post}'",[89,368,340],{"class":226},[89,370,280],{"class":112},[89,372,258],{"class":219},[89,374,252],{"class":226},[89,376,377],{"class":99},"'update'",[89,379,352],{"class":226},[89,381,382,384,386,389,391,393,395,397,399,401,404],{"class":91,"line":264},[89,383,328],{"class":112},[89,385,241],{"class":219},[89,387,388],{"class":95},"delete",[89,390,246],{"class":226},[89,392,366],{"class":99},[89,394,340],{"class":226},[89,396,280],{"class":112},[89,398,258],{"class":219},[89,400,252],{"class":226},[89,402,403],{"class":99},"'destroy'",[89,405,352],{"class":226},[89,407,409],{"class":91,"line":408},5,[89,410,411],{"class":226},"});\n",[15,413,414],{},"Cette approche évite de mélanger les routes publiques et les routes privées.",[10,416,418],{"id":417},"construire-un-contrôleur-simple-et-lisible","Construire un contrôleur simple et lisible",[15,420,421],{},"Un contrôleur d'API doit rester léger. Il orchestre les actions, mais il ne doit pas contenir toute la logique métier.",[80,423,425],{"className":211,"code":424,"language":128,"meta":85,"style":85},"use App\\Http\\Requests\\StorePostRequest;\nuse App\\Http\\Resources\\PostResource;\nuse App\\Models\\Post;\nuse Illuminate\\Http\\JsonResponse;\n\nclass PostController extends Controller\n{\n    public function index(): JsonResponse\n    {\n        $posts = Post::latest()->paginate(10);\n\n        return response()->json(PostResource::collection($posts));\n    }\n\n    public function store(StorePostRequest $request): JsonResponse\n    {\n        $post = Post::create($request->validated());\n\n        return (new PostResource($post))\n            ->response()\n            ->setStatusCode(201);\n    }\n\n    public function show(Post $post): PostResource\n    {\n        return new PostResource($post);\n    }\n}\n",[35,426,427,436,445,454,463,467,482,488,509,515,545,550,579,585,590,612,617,643,648,665,677,692,697,702,724,729,742,747],{"__ignoreMap":85},[89,428,429,431,434],{"class":91,"line":92},[89,430,220],{"class":219},[89,432,433],{"class":112}," App\\Http\\Requests\\StorePostRequest",[89,435,227],{"class":226},[89,437,438,440,443],{"class":91,"line":109},[89,439,220],{"class":219},[89,441,442],{"class":112}," App\\Http\\Resources\\PostResource",[89,444,227],{"class":226},[89,446,447,449,452],{"class":91,"line":155},[89,448,220],{"class":219},[89,450,451],{"class":112}," App\\Models\\Post",[89,453,227],{"class":226},[89,455,456,458,461],{"class":91,"line":264},[89,457,220],{"class":219},[89,459,460],{"class":112}," Illuminate\\Http\\JsonResponse",[89,462,227],{"class":226},[89,464,465],{"class":91,"line":408},[89,466,233],{"emptyLinePlaceholder":232},[89,468,470,473,476,479],{"class":91,"line":469},6,[89,471,472],{"class":219},"class",[89,474,475],{"class":95}," PostController",[89,477,478],{"class":219}," extends",[89,480,481],{"class":95}," Controller\n",[89,483,485],{"class":91,"line":484},7,[89,486,487],{"class":226},"{\n",[89,489,491,494,497,500,503,506],{"class":91,"line":490},8,[89,492,493],{"class":219},"    public",[89,495,496],{"class":219}," function",[89,498,499],{"class":95}," index",[89,501,502],{"class":226},"()",[89,504,505],{"class":219},":",[89,507,508],{"class":112}," JsonResponse\n",[89,510,512],{"class":91,"line":511},9,[89,513,514],{"class":226},"    {\n",[89,516,518,521,524,526,528,531,533,535,538,540,543],{"class":91,"line":517},10,[89,519,520],{"class":226},"        $posts ",[89,522,523],{"class":219},"=",[89,525,137],{"class":112},[89,527,241],{"class":219},[89,529,530],{"class":95},"latest",[89,532,502],{"class":226},[89,534,312],{"class":219},[89,536,537],{"class":95},"paginate",[89,539,246],{"class":226},[89,541,542],{"class":112},"10",[89,544,261],{"class":226},[89,546,548],{"class":91,"line":547},11,[89,549,233],{"emptyLinePlaceholder":232},[89,551,553,556,559,561,563,566,568,571,573,576],{"class":91,"line":552},12,[89,554,555],{"class":219},"        return",[89,557,558],{"class":95}," response",[89,560,502],{"class":226},[89,562,312],{"class":219},[89,564,565],{"class":95},"json",[89,567,246],{"class":226},[89,569,570],{"class":112},"PostResource",[89,572,241],{"class":219},[89,574,575],{"class":95},"collection",[89,577,578],{"class":226},"($posts));\n",[89,580,582],{"class":91,"line":581},13,[89,583,584],{"class":226},"    }\n",[89,586,588],{"class":91,"line":587},14,[89,589,233],{"emptyLinePlaceholder":232},[89,591,593,595,597,600,602,605,608,610],{"class":91,"line":592},15,[89,594,493],{"class":219},[89,596,496],{"class":219},[89,598,599],{"class":95}," store",[89,601,246],{"class":226},[89,603,604],{"class":112},"StorePostRequest",[89,606,607],{"class":226}," $request)",[89,609,505],{"class":219},[89,611,508],{"class":112},[89,613,615],{"class":91,"line":614},16,[89,616,514],{"class":226},[89,618,620,623,625,627,629,632,635,637,640],{"class":91,"line":619},17,[89,621,622],{"class":226},"        $post ",[89,624,523],{"class":219},[89,626,137],{"class":112},[89,628,241],{"class":219},[89,630,631],{"class":95},"create",[89,633,634],{"class":226},"($request",[89,636,312],{"class":219},[89,638,639],{"class":95},"validated",[89,641,642],{"class":226},"());\n",[89,644,646],{"class":91,"line":645},18,[89,647,233],{"emptyLinePlaceholder":232},[89,649,651,653,656,659,662],{"class":91,"line":650},19,[89,652,555],{"class":219},[89,654,655],{"class":226}," (",[89,657,658],{"class":219},"new",[89,660,661],{"class":112}," PostResource",[89,663,664],{"class":226},"($post))\n",[89,666,668,671,674],{"class":91,"line":667},20,[89,669,670],{"class":219},"            ->",[89,672,673],{"class":95},"response",[89,675,676],{"class":226},"()\n",[89,678,680,682,685,687,690],{"class":91,"line":679},21,[89,681,670],{"class":219},[89,683,684],{"class":95},"setStatusCode",[89,686,246],{"class":226},[89,688,689],{"class":112},"201",[89,691,261],{"class":226},[89,693,695],{"class":91,"line":694},22,[89,696,584],{"class":226},[89,698,700],{"class":91,"line":699},23,[89,701,233],{"emptyLinePlaceholder":232},[89,703,705,707,709,712,714,717,720,722],{"class":91,"line":704},24,[89,706,493],{"class":219},[89,708,496],{"class":219},[89,710,711],{"class":95}," show",[89,713,246],{"class":226},[89,715,716],{"class":112},"Post",[89,718,719],{"class":226}," $post)",[89,721,505],{"class":219},[89,723,165],{"class":112},[89,725,727],{"class":91,"line":726},25,[89,728,514],{"class":226},[89,730,732,734,737,739],{"class":91,"line":731},26,[89,733,555],{"class":219},[89,735,736],{"class":219}," new",[89,738,661],{"class":112},[89,740,741],{"class":226},"($post);\n",[89,743,745],{"class":91,"line":744},27,[89,746,584],{"class":226},[89,748,750],{"class":91,"line":749},28,[89,751,752],{"class":226},"}\n",[15,754,755],{},"Quelques points importants ici :",[29,757,758,765,774],{},[32,759,760,761,764],{},"la méthode ",[35,762,763],{},"index"," renvoie une liste paginée",[32,766,760,767,770,771],{},[35,768,769],{},"store"," renvoie un statut ",[35,772,773],{},"201 Created",[32,775,776,777],{},"le route model binding de Laravel charge automatiquement le bon article pour ",[35,778,779],{},"show",[15,781,782],{},"Cette simplicité rend le contrôleur facile à relire et à tester.",[10,784,786],{"id":785},"authentification-avec-sanctum","Authentification avec Sanctum",[15,788,789],{},"Si votre API doit être utilisée par une application mobile, un front Vue ou une SPA, Laravel Sanctum est souvent le meilleur point de départ. Il permet de gérer l'authentification de manière légère, sans complexité inutile.",[15,791,792],{},"Installez Sanctum puis publiez sa configuration :",[80,794,796],{"className":82,"code":795,"language":84,"meta":85,"style":85},"composer require laravel\u002Fsanctum\nphp artisan vendor:publish --provider=\"Laravel\\Sanctum\\SanctumServiceProvider\"\n",[35,797,798,808],{"__ignoreMap":85},[89,799,800,802,805],{"class":91,"line":92},[89,801,96],{"class":95},[89,803,804],{"class":99}," require",[89,806,807],{"class":99}," laravel\u002Fsanctum\n",[89,809,810,812,814,817,820],{"class":91,"line":109},[89,811,128],{"class":95},[89,813,131],{"class":99},[89,815,816],{"class":99}," vendor:publish",[89,818,819],{"class":112}," --provider=",[89,821,822],{"class":99},"\"Laravel\\Sanctum\\SanctumServiceProvider\"\n",[15,824,825],{},"Ensuite, protégez les routes qui nécessitent un utilisateur connecté :",[80,827,829],{"className":211,"code":828,"language":128,"meta":85,"style":85},"Route::middleware('auth:sanctum')->group(function () {\n    Route::apiResource('posts', PostController::class);\n});\n",[35,830,831,855,875],{"__ignoreMap":85},[89,832,833,835,837,839,841,843,845,847,849,851,853],{"class":91,"line":92},[89,834,238],{"class":112},[89,836,241],{"class":219},[89,838,301],{"class":95},[89,840,246],{"class":226},[89,842,306],{"class":99},[89,844,309],{"class":226},[89,846,312],{"class":219},[89,848,315],{"class":95},[89,850,246],{"class":226},[89,852,320],{"class":219},[89,854,323],{"class":226},[89,856,857,859,861,863,865,867,869,871,873],{"class":91,"line":109},[89,858,328],{"class":112},[89,860,241],{"class":219},[89,862,207],{"class":95},[89,864,246],{"class":226},[89,866,275],{"class":99},[89,868,252],{"class":226},[89,870,280],{"class":112},[89,872,258],{"class":219},[89,874,261],{"class":226},[89,876,877],{"class":91,"line":155},[89,878,411],{"class":226},[15,880,881],{},"Le principe est simple :",[29,883,884,887],{},[32,885,886],{},"les routes publiques servent à lire les contenus visibles par tous",[32,888,889],{},"les routes protégées servent à créer, modifier ou supprimer des données",[15,891,892,893,50,896,899],{},"Pensez aussi à retourner des codes HTTP cohérents. Une authentification refusée doit produire une réponse claire, par exemple ",[35,894,895],{},"401 Unauthorized",[35,897,898],{},"403 Forbidden"," selon le cas.",[10,901,903],{"id":902},"validation-et-form-requests","Validation et Form Requests",[15,905,906,907,910],{},"Une erreur fréquente consiste à mettre les règles de validation directement dans le contrôleur. Cela fonctionne, mais le code devient vite difficile à lire. Les ",[35,908,909],{},"FormRequest"," permettent de centraliser cette logique et de garder le contrôleur propre.",[80,912,914],{"className":211,"code":913,"language":128,"meta":85,"style":85},"namespace App\\Http\\Requests;\n\nuse Illuminate\\Foundation\\Http\\FormRequest;\n\nclass StorePostRequest extends FormRequest\n{\n    public function authorize(): bool\n    {\n        return true;\n    }\n\n    public function rules(): array\n    {\n        return [\n            'title' => ['required', 'string', 'max:255'],\n            'content' => ['required', 'string'],\n            'tags' => ['sometimes', 'array'],\n        ];\n    }\n}\n",[35,915,916,926,930,939,943,955,959,975,979,988,992,996,1012,1016,1023,1050,1067,1086,1091,1095],{"__ignoreMap":85},[89,917,918,921,924],{"class":91,"line":92},[89,919,920],{"class":219},"namespace",[89,922,923],{"class":95}," App\\Http\\Requests",[89,925,227],{"class":226},[89,927,928],{"class":91,"line":109},[89,929,233],{"emptyLinePlaceholder":232},[89,931,932,934,937],{"class":91,"line":155},[89,933,220],{"class":219},[89,935,936],{"class":112}," Illuminate\\Foundation\\Http\\FormRequest",[89,938,227],{"class":226},[89,940,941],{"class":91,"line":264},[89,942,233],{"emptyLinePlaceholder":232},[89,944,945,947,950,952],{"class":91,"line":408},[89,946,472],{"class":219},[89,948,949],{"class":95}," StorePostRequest",[89,951,478],{"class":219},[89,953,954],{"class":95}," FormRequest\n",[89,956,957],{"class":91,"line":469},[89,958,487],{"class":226},[89,960,961,963,965,968,970,972],{"class":91,"line":484},[89,962,493],{"class":219},[89,964,496],{"class":219},[89,966,967],{"class":95}," authorize",[89,969,502],{"class":226},[89,971,505],{"class":219},[89,973,974],{"class":219}," bool\n",[89,976,977],{"class":91,"line":490},[89,978,514],{"class":226},[89,980,981,983,986],{"class":91,"line":511},[89,982,555],{"class":219},[89,984,985],{"class":112}," true",[89,987,227],{"class":226},[89,989,990],{"class":91,"line":517},[89,991,584],{"class":226},[89,993,994],{"class":91,"line":547},[89,995,233],{"emptyLinePlaceholder":232},[89,997,998,1000,1002,1005,1007,1009],{"class":91,"line":552},[89,999,493],{"class":219},[89,1001,496],{"class":219},[89,1003,1004],{"class":95}," rules",[89,1006,502],{"class":226},[89,1008,505],{"class":219},[89,1010,1011],{"class":219}," array\n",[89,1013,1014],{"class":91,"line":581},[89,1015,514],{"class":226},[89,1017,1018,1020],{"class":91,"line":587},[89,1019,555],{"class":219},[89,1021,1022],{"class":226}," [\n",[89,1024,1025,1028,1031,1034,1037,1039,1042,1044,1047],{"class":91,"line":592},[89,1026,1027],{"class":99},"            'title'",[89,1029,1030],{"class":219}," =>",[89,1032,1033],{"class":226}," [",[89,1035,1036],{"class":99},"'required'",[89,1038,252],{"class":226},[89,1040,1041],{"class":99},"'string'",[89,1043,252],{"class":226},[89,1045,1046],{"class":99},"'max:255'",[89,1048,1049],{"class":226},"],\n",[89,1051,1052,1055,1057,1059,1061,1063,1065],{"class":91,"line":614},[89,1053,1054],{"class":99},"            'content'",[89,1056,1030],{"class":219},[89,1058,1033],{"class":226},[89,1060,1036],{"class":99},[89,1062,252],{"class":226},[89,1064,1041],{"class":99},[89,1066,1049],{"class":226},[89,1068,1069,1072,1074,1076,1079,1081,1084],{"class":91,"line":619},[89,1070,1071],{"class":99},"            'tags'",[89,1073,1030],{"class":219},[89,1075,1033],{"class":226},[89,1077,1078],{"class":99},"'sometimes'",[89,1080,252],{"class":226},[89,1082,1083],{"class":99},"'array'",[89,1085,1049],{"class":226},[89,1087,1088],{"class":91,"line":645},[89,1089,1090],{"class":226},"        ];\n",[89,1092,1093],{"class":91,"line":650},[89,1094,584],{"class":226},[89,1096,1097],{"class":91,"line":667},[89,1098,752],{"class":226},[15,1100,1101],{},"Pourquoi c'est mieux ?",[29,1103,1104,1107,1110],{},[32,1105,1106],{},"les règles sont regroupées à un seul endroit",[32,1108,1109],{},"les messages d'erreur sont automatiquement renvoyés en JSON si la requête attend du JSON",[32,1111,1112],{},"vous pouvez réutiliser la même logique dans plusieurs actions si nécessaire",[15,1114,1115,1116,1119],{},"Avec cette base, le contrôleur peut simplement faire confiance aux données validées via ",[35,1117,1118],{},"validated()",".",[10,1121,1123],{"id":1122},"retourner-des-réponses-cohérentes","Retourner des réponses cohérentes",[15,1125,1126],{},"Une API utile ne renvoie pas seulement des données. Elle renvoie aussi une structure prévisible. Essayez de garder la même forme pour les réponses de lecture, de création et d'erreur.",[15,1128,1129],{},"Par exemple :",[29,1131,1132,1138,1143,1149,1155],{},[32,1133,1134,1137],{},[35,1135,1136],{},"200 OK"," pour une lecture réussie",[32,1139,1140,1142],{},[35,1141,773],{}," pour une création",[32,1144,1145,1148],{},[35,1146,1147],{},"204 No Content"," pour une suppression réussie",[32,1150,1151,1154],{},[35,1152,1153],{},"422 Unprocessable Entity"," pour une validation invalide",[32,1156,1157,1160],{},[35,1158,1159],{},"404 Not Found"," si la ressource n'existe pas",[15,1162,1163],{},"Laravel facilite cette cohérence avec les API Resources :",[80,1165,1167],{"className":211,"code":1166,"language":128,"meta":85,"style":85},"use Illuminate\\Http\\Resources\\Json\\JsonResource;\n\nclass PostResource extends JsonResource\n{\n    public function toArray($request): array\n    {\n        return [\n            'id' => $this->id,\n            'title' => $this->title,\n            'content' => $this->content,\n            'created_at' => $this->created_at?->toDateTimeString(),\n        ];\n    }\n}\n",[35,1168,1169,1178,1182,1193,1197,1213,1217,1223,1238,1251,1264,1287,1291,1295],{"__ignoreMap":85},[89,1170,1171,1173,1176],{"class":91,"line":92},[89,1172,220],{"class":219},[89,1174,1175],{"class":112}," Illuminate\\Http\\Resources\\Json\\JsonResource",[89,1177,227],{"class":226},[89,1179,1180],{"class":91,"line":109},[89,1181,233],{"emptyLinePlaceholder":232},[89,1183,1184,1186,1188,1190],{"class":91,"line":155},[89,1185,472],{"class":219},[89,1187,661],{"class":95},[89,1189,478],{"class":219},[89,1191,1192],{"class":95}," JsonResource\n",[89,1194,1195],{"class":91,"line":264},[89,1196,487],{"class":226},[89,1198,1199,1201,1203,1206,1209,1211],{"class":91,"line":408},[89,1200,493],{"class":219},[89,1202,496],{"class":219},[89,1204,1205],{"class":95}," toArray",[89,1207,1208],{"class":226},"($request)",[89,1210,505],{"class":219},[89,1212,1011],{"class":219},[89,1214,1215],{"class":91,"line":469},[89,1216,514],{"class":226},[89,1218,1219,1221],{"class":91,"line":484},[89,1220,555],{"class":219},[89,1222,1022],{"class":226},[89,1224,1225,1228,1230,1233,1235],{"class":91,"line":490},[89,1226,1227],{"class":99},"            'id'",[89,1229,1030],{"class":219},[89,1231,1232],{"class":112}," $this",[89,1234,312],{"class":219},[89,1236,1237],{"class":226},"id,\n",[89,1239,1240,1242,1244,1246,1248],{"class":91,"line":511},[89,1241,1027],{"class":99},[89,1243,1030],{"class":219},[89,1245,1232],{"class":112},[89,1247,312],{"class":219},[89,1249,1250],{"class":226},"title,\n",[89,1252,1253,1255,1257,1259,1261],{"class":91,"line":517},[89,1254,1054],{"class":99},[89,1256,1030],{"class":219},[89,1258,1232],{"class":112},[89,1260,312],{"class":219},[89,1262,1263],{"class":226},"content,\n",[89,1265,1266,1269,1271,1273,1275,1278,1281,1284],{"class":91,"line":547},[89,1267,1268],{"class":99},"            'created_at'",[89,1270,1030],{"class":219},[89,1272,1232],{"class":112},[89,1274,312],{"class":219},[89,1276,1277],{"class":226},"created_at",[89,1279,1280],{"class":219},"?->",[89,1282,1283],{"class":95},"toDateTimeString",[89,1285,1286],{"class":226},"(),\n",[89,1288,1289],{"class":91,"line":552},[89,1290,1090],{"class":226},[89,1292,1293],{"class":91,"line":581},[89,1294,584],{"class":226},[89,1296,1297],{"class":91,"line":587},[89,1298,752],{"class":226},[15,1300,1301],{},"Cela vous évite d'exposer directement toutes les colonnes du modèle.",[10,1303,1305],{"id":1304},"gestion-centralisée-des-erreurs","Gestion centralisée des erreurs",[15,1307,1308],{},"La gestion des erreurs est essentielle, car une API mal gérée devient vite frustrante à consommer. L'objectif est d'afficher un message utile, sans exposer de détails internes inutiles.",[15,1310,1311],{},"Dans Laravel, beaucoup de cas sont déjà bien pris en charge :",[29,1313,1314,1320,1326],{},[32,1315,1316,1317],{},"les erreurs de validation retournent automatiquement un ",[35,1318,1319],{},"422",[32,1321,1322,1323],{},"le route model binding peut renvoyer un ",[35,1324,1325],{},"404",[32,1327,1328],{},"Sanctum gère les réponses d'authentification non valides",[15,1330,1331],{},"Pour vos erreurs métier, créez des exceptions explicites et traduisez-les en réponses JSON propres. Le principe important est de ne jamais mélanger un message technique brut avec un message destiné au client.",[15,1333,1334],{},"Exemple de logique à garder en tête :",[80,1336,1338],{"className":211,"code":1337,"language":128,"meta":85,"style":85},"\u002F\u002F Si l'erreur concerne une validation, Laravel renvoie déjà un JSON clair.\n\u002F\u002F Si l'erreur est métier, traduisez-la en message compréhensible pour le client.\n",[35,1339,1340,1346],{"__ignoreMap":85},[89,1341,1342],{"class":91,"line":92},[89,1343,1345],{"class":1344},"sJ8bj","\u002F\u002F Si l'erreur concerne une validation, Laravel renvoie déjà un JSON clair.\n",[89,1347,1348],{"class":91,"line":109},[89,1349,1350],{"class":1344},"\u002F\u002F Si l'erreur est métier, traduisez-la en message compréhensible pour le client.\n",[15,1352,1353],{},"Le point clé n'est pas la forme exacte du code, mais la discipline : une erreur doit toujours être lisible et exploitable côté client.",[10,1355,1357],{"id":1356},"pagination-filtres-et-tri","Pagination, filtres et tri",[15,1359,1360],{},"Une API de blog devient vite plus utile si elle permet de paginer et de filtrer les résultats. C'est particulièrement important lorsque la table commence à grandir.",[15,1362,1363],{},"Bonnes pratiques simples :",[29,1365,1366,1373,1386],{},[32,1367,1368,1369,1372],{},"utilisez ",[35,1370,1371],{},"paginate()"," plutôt que de tout charger d'un seul coup",[32,1374,1375,1376,252,1379,50,1382,1385],{},"filtrez par ",[35,1377,1378],{},"status",[35,1380,1381],{},"tag",[35,1383,1384],{},"author"," si nécessaire",[32,1387,1388],{},"triez systématiquement les résultats pour éviter les réponses imprévisibles",[15,1390,1129],{},[80,1392,1394],{"className":211,"code":1393,"language":128,"meta":85,"style":85},"Post::query()\n    ->when(request('tag'), fn ($query, $tag) => $query->whereJsonContains('tags', $tag))\n    ->latest()\n    ->paginate(10);\n",[35,1395,1396,1407,1447,1455],{"__ignoreMap":85},[89,1397,1398,1400,1402,1405],{"class":91,"line":92},[89,1399,716],{"class":112},[89,1401,241],{"class":219},[89,1403,1404],{"class":95},"query",[89,1406,676],{"class":226},[89,1408,1409,1412,1415,1417,1420,1422,1425,1428,1431,1434,1436,1439,1441,1444],{"class":91,"line":109},[89,1410,1411],{"class":219},"    ->",[89,1413,1414],{"class":95},"when",[89,1416,246],{"class":226},[89,1418,1419],{"class":95},"request",[89,1421,246],{"class":226},[89,1423,1424],{"class":99},"'tag'",[89,1426,1427],{"class":226},"), ",[89,1429,1430],{"class":219},"fn",[89,1432,1433],{"class":226}," ($query, $tag) => $query",[89,1435,312],{"class":219},[89,1437,1438],{"class":95},"whereJsonContains",[89,1440,246],{"class":226},[89,1442,1443],{"class":99},"'tags'",[89,1445,1446],{"class":226},", $tag))\n",[89,1448,1449,1451,1453],{"class":91,"line":155},[89,1450,1411],{"class":219},[89,1452,530],{"class":95},[89,1454,676],{"class":226},[89,1456,1457,1459,1461,1463,1465],{"class":91,"line":264},[89,1458,1411],{"class":219},[89,1460,537],{"class":95},[89,1462,246],{"class":226},[89,1464,542],{"class":112},[89,1466,261],{"class":226},[10,1468,1470],{"id":1469},"conclusion","Conclusion",[15,1472,1473],{},"Une API Laravel solide repose sur quelques principes simples : des routes explicites, des contrôleurs légers, une validation centralisée, une authentification claire et des réponses JSON cohérentes.",[15,1475,1476],{},"Si vous respectez cette structure dès le départ, votre API restera facile à comprendre, à tester et à faire évoluer. Le vrai gain ne vient pas seulement du fait que le code fonctionne, mais du fait qu'il reste lisible lorsque le projet grandit.",[1478,1479,1480],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":85,"searchDepth":109,"depth":109,"links":1482},[1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493],{"id":12,"depth":109,"text":13},{"id":23,"depth":109,"text":24},{"id":70,"depth":109,"text":71},{"id":185,"depth":109,"text":186},{"id":417,"depth":109,"text":418},{"id":785,"depth":109,"text":786},{"id":902,"depth":109,"text":903},{"id":1122,"depth":109,"text":1123},{"id":1304,"depth":109,"text":1305},{"id":1356,"depth":109,"text":1357},{"id":1469,"depth":109,"text":1470},null,"Un guide pas à pas pour structurer une API Laravel lisible, sécurisée et facile à faire évoluer, avec les routes, la validation, l'authentification et la gestion des erreurs.","md",{},"\u002Fblog\u002Fconstruire-une-api-restful-avec-laravel","2025-03-15",{"title":5,"description":1495},"blog\u002Fconstruire-une-api-restful-avec-laravel",[1503,1504,1505,1506],"Laravel","PHP","API","Backend","-YcZel08qaIbp4jORs9a0akyXzQQm9T5CrwO066Ay44",1788219651547]