بناء API بصيغة JSON في Laravel
مدونتك حتى الآن ترد على المتصفح بصفحات HTML. لكن ماذا لو أردت تطبيق موبايل يعرض المقالات نفسها؟ التطبيق لا يريد صفحة جاهزة، بل يريد البيانات وحدها ليعرضها بطريقته. في هذا الدرس ستبني API: مسارات ترد بصيغة JSON يفهمها أي تطبيق، سواء كُتب بـ Flutter أو JavaScript أو غيرهما.
ما هو الـ API؟
Section titled “ما هو الـ API؟”الـ API (واجهة برمجة التطبيقات) طريقة يتحدث بها برنامج مع برنامج آخر. في المطعم أنت لا تدخل المطبخ: تطلب من النادل بلغة متفق عليها، فيعود إليك بالطبق. الـ API هو النادل، والمطبخ هو قاعدة بياناتك، والطبق بيانات بصيغة JSON: نص منظم من مفاتيح وقيم.
الصفحات تبقى في routes/web.php، والـ API له ملف مستقل تفعّله بأمر واحد:
php artisan install:apiالأمر ينشئ الملف routes/api.php ويسجّله في bootstrap/app.php، ويثبّت حزمة Sanctum التي تحمي الـ API لاحقاً. ولمسارات هذا الملف ثلاث خصائص:
- كل مسار يبدأ تلقائياً بـ
/api. - لا جلسات ولا كوكيز، ولذلك لا تحتاج
@csrf. - الطلب لا يحمل «ذاكرة»: كل طلب مستقل بنفسه.
أول مسار
Section titled “أول مسار”في Laravel يكفي أن تُرجع مصفوفة ليحوّلها إلى JSON:
Route::get('/ping', function () { return ['message' => 'الخادم يعمل', 'time' => now()->format('H:i')];});شغّل php artisan serve وافتح http://localhost:8000/api/ping:
{"message":"الخادم يعمل","time":"10:30"}والأمر نفسه مع Eloquent: لو أرجعت Post::latest()->get() لحصلت على كل المقالات بكل أعمدتها.
خمسة مسارات بسطر واحد
Section titled “خمسة مسارات بسطر واحد”أي API يدير «شيئاً» يحتاج العمليات الخمس نفسها: عرض الكل، وعرض واحد، وإضافة، وتعديل، وحذف. أنشئ متحكماً مجهزاً لها:
php artisan make:controller Api/PostController --apiثم سجّل مساراته كلها بسطر واحد:
use App\Http\Controllers\Api\PostController;
Route::apiResource('posts', PostController::class);وتأكد بالأمر php artisan route:list --path=api/posts:
GET|HEAD api/posts ......... posts.index › Api\PostController@indexPOST api/posts ......... posts.store › Api\PostController@storeGET|HEAD api/posts/{post} .. posts.show › Api\PostController@showPUT|PATCH api/posts/{post} .. posts.update › Api\PostController@updateDELETE api/posts/{post} .. posts.destroy › Api\PostController@destroyلاحظ أن الرابط api/posts نفسه يعني «اعرض» مع GET و«أضف» مع POST. طريقة الطلب هي الفعل، والرابط هو الشيء.
المتحكم
Section titled “المتحكم”<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;use App\Models\Post;use Illuminate\Http\Request;
class PostController extends Controller{ public function index() { return Post::latest()->paginate(10); }
public function show(Post $post) { return $post; }
public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|min:5|max:100', 'body' => 'required|min:20', ]);
$post = Post::create($validated);
return response()->json($post, 201); }
public function update(Request $request, Post $post) { $validated = $request->validate([ 'title' => 'required|min:5|max:100', 'body' => 'required|min:20', ]);
$post->update($validated);
return $post; }
public function destroy(Post $post) { $post->delete();
return response()->noContent(); }}فيه ثلاثة أشياء جديدة:
- ربط الـ Model بالمسار (Route Model Binding): كتبنا
Post $postبدلint $id. Laravel يرى أن اسم المتغير يطابق{post}في المسار، فيجلب المقال بنفسه. وإن لم يجده ردّ بـ 404 دون أن تكتبfindOrFail. paginate(10): يُرجع عشرة مقالات فقط، ومعها معلومات الصفحات. تطبيق الموبايل يطلب الصفحة التالية بـ?page=2.- رموز الحالة: الرد لا يحمل البيانات فقط، بل رقماً يلخص ما حدث.
| الرمز | معناه | متى نرسله |
|---|---|---|
| 200 | نجح الطلب | العرض والتعديل (الافتراضي) |
| 201 | أُنشئ عنصر جديد | بعد الإضافة |
| 204 | نجح ولا محتوى | بعد الحذف |
| 404 | غير موجود | مقال برقم لا وجود له |
| 422 | بيانات غير صالحة | فشل التحقق |
الأخطاء أيضاً JSON
Section titled “الأخطاء أيضاً JSON”أرسل طلب POST إلى /api/posts بعنوان قصير وبلا محتوى. في تطبيق الصفحات كان Laravel يعيدك إلى النموذج، أما هنا فيرد بالرمز 422 وهذا الجسم:
{ "message": "The title field must be at least 5 characters. (and 1 more error)", "errors": { "title": ["The title field must be at least 5 characters."], "body": ["The body field is required."] }}قواعد التحقق لم تتغير، لكن الرد صار بيانات يعرضها التطبيق تحت كل حقل.
هكذا تجرّب الإضافة من الطرفية:
curl -X POST http://localhost:8000/api/posts \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"title":"abc"}'والأسهل برنامج بواجهة رسومية مثل Postman أو Insomnia: تختار طريقة الطلب وتكتب JSON وترى الرد منسقاً.
تشكيل الرد بـ API Resources
Section titled “تشكيل الرد بـ API Resources”إرجاع الـ Model مباشرة يكشف كل أعمدة الجدول، وأي تغيير في قاعدة البيانات يكسر التطبيقات التي تعتمد على الشكل القديم. الحل طبقة بينهما تحدد أنت فيها شكل الرد:
php artisan make:resource PostResourcepublic function toArray(Request $request): array{ return [ 'id' => $this->id, 'title' => $this->title, 'body' => $this->body, 'created' => $this->created_at->format('Y-m-d'), ];}ثم استخدمه في المتحكم (مع use App\Http\Resources\PostResource; في أعلى الملف):
public function index(){ return PostResource::collection(Post::latest()->paginate(10));}
public function show(Post $post){ return new PostResource($post);}الآن GET /api/posts/1 يرد بالحقول التي اخترتها فقط، داخل المفتاح data:
{ "data": { "id": 1, "title": "أول مقال في المدونة", "body": "مرحباً بكم في مدونتي الجديدة عن البرمجة.", "created": "2026-11-12" }}وفي store أرجع (new PostResource($post))->response()->setStatusCode(201) ليبقى الرمز 201. أما قائمة المقالات فتأتي مصفوفة داخل data، وبجانبها المفتاحان links و meta وفيهما رقم الصفحة الحالية وعدد المقالات الكلي.
جرّب بنفسك
Section titled “جرّب بنفسك”- أضف مسار
/api/pingوافتحه في المتصفح، ثم افتح/api/postsوقارن الرد قبل استخدامPostResourceوبعده. - اطلب
/api/posts/999مرة من المتصفح ومرة بـcurlمع ترويسةAccept: application/json، وقارن الردين. - أضف إلى الرد عدد التعليقات: استخدم
Post::withCount('comments')في المتحكم، و'comments_count' => $this->comments_countفي الـ Resource. - تحدٍّ: أضف
Route::apiResource('posts.comments', ...)بمتحكم جديد يعرض تعليقات مقال ويضيف إليها.
أخطاء شائعة
Section titled “أخطاء شائعة”- كتابة المسارات في
web.php: مسارات الصفحات تطلب@csrfمع كل POST، فيفشل طلب التطبيق بالخطأ 419. مكان الـ API هوroutes/api.php. - نسيان البادئة: المسار المكتوب
/postsفيapi.phpعنوانه الفعلي/api/posts. - إرجاع 200 دائماً: التطبيق يقرأ الرمز قبل الجسم. استخدم 201 للإضافة و 204 للحذف، واترك 404 و 422 لـ Laravel.
- إرجاع الـ Model كما هو: قد يتسرب عمود لم تقصد نشره. حدد الحقول بـ Resource.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
كتبت في routes/api.php المسار Route::get('/posts', ...). ما العنوان الذي يطلبه التطبيق؟
كل مسارات routes/api.php تأخذ البادئة /api تلقائياً.
سؤال
أرسل التطبيق مقالاً جديداً بعنوان قصير جداً، ومعه الترويسة Accept: application/json. بماذا يرد Laravel؟
فشل التحقق في طلب يريد JSON يعطي 422 ومعه المفتاح errors وفيه رسائل كل حقل.
الخلاصة
Section titled “الخلاصة”- الـ API يرد ببيانات JSON بدل صفحات HTML، ومساراته في
routes/api.phpوتبدأ بـ/api. Route::apiResourceيسجّل مسارات العرض والإضافة والتعديل والحذف بسطر واحد.- كتابة
Post $postفي الدالة تجلب المقال تلقائياً، وترد بـ 404 إن لم يوجد. - رموز الحالة جزء من الرد: 201 للإنشاء، و 204 للحذف، و 422 لفشل التحقق.
- API Resources تحدد شكل الرد، وتفصل تطبيقاتك عن شكل قاعدة البيانات.