بناء REST API باستخدام Laravel خطوة بخطوة
مقدمة REST API باستخدام Laravel
إذا كنت قد عملت سابقًا على تطبيق ويب في Laravel، فغالبًا أنت تعرف كم هو مريح هذا الإطار عندما يتعلق الأمر ببناء منطق الأعمال بسرعة وبشكل منظم. لكن عندما ينتقل المشروع من صفحات Blade التقليدية إلى واجهة أمامية مستقلة في React أو Vue أو تطبيق موبايل، يصبح REST API هو الجسر الحقيقي بين البيانات والمستخدم. هنا تبدأ الحكاية الممتعة: كيف نبني API نظيفًا، واضحًا، سهل التطوير، وآمنًا في الوقت نفسه؟ هذا المقال يأخذك من الصفر إلى مستوى عملي يسمح لك ببناء REST API احترافي باستخدام Laravel خطوة بخطوة، مع أمثلة واقعية تشرح الفكرة بدل أن تكتفي بعرض الأكواد فقط.
سنفترض في هذا المقال أننا نبني API لإدارة مقالات أو منتجات، لأن هذا النوع من المشاريع بسيط بما يكفي لشرح الفكرة، وفي الوقت نفسه غني بكل ما تحتاجه من CRUD، التحقق من البيانات، إعادة تنظيم الاستجابات، المصادقة، الاختبار، والتعامل مع الأخطاء. والأهم من ذلك كله أننا لن نتعامل مع Laravel كأنه مجرد أوامر محفوظة، بل كمنهج تفكير يسهّل عليك الحياة عندما يكبر المشروع وتصبح التفاصيل أكثر من أن تُحفظ في الذاكرة.
ما هو REST API ولماذا نستخدمه؟
REST API هو أسلوب معماري للتواصل بين الأنظمة عبر HTTP. ببساطة، نحن نعرض موارد مثل المستخدمين، المنتجات، الطلبات، أو المقالات على شكل endpoints، ويقوم العميل بإرسال طلبات مثل GET وPOST وPUT وDELETE للتعامل مع هذه الموارد. الجميل في REST أنه واضح ومنظم، ويمكن استهلاكه بسهولة من أي واجهة أمامية أو تطبيق خارجي.
في المشاريع الحديثة، أصبح REST API من أكثر الطرق استخدامًا لعدة أسباب. أولها أن الفصل بين الواجهة الأمامية والخلفية يمنحك مرونة كبيرة. يمكنك بناء backend في Laravel، ثم استخدامه مع React أو Flutter أو Next.js أو حتى تطبيقات سطح المكتب. السبب الثاني أن JSON أصبح اللغة المشتركة تقريبًا بين معظم الأنظمة. السبب الثالث أن REST API قابل للتوسع، ويمكن تنظيمه بشكل يجعل فريق العمل يتعاون دون فوضى. أما السبب الرابع فهو أن Laravel يقدم لك أدوات ممتازة جدًا لبناء API نظيف مع أقل قدر من التعقيد.
عندما يبدأ المطور مبتدئًا في بناء API، يقع غالبًا في فخ "العمل السريع". ينشئ route هنا، وquery هناك، ويعيد return response()->json(...) من كل مكان. هذا يعمل مؤقتًا، لكنه يصبح فوضويًا جدًا عند أول توسع. لهذا سنبني الأمور من البداية بشكل صحيح: تنظيم routes، controllers، validation، resources، authentication، error handling، testing، ثم بعض الممارسات التي تمنحك API يليق بالمشاريع الحقيقية.
تهيئة المشروع وتجهيز البيئة
قبل أن نبدأ في كتابة أي endpoint، نحتاج إلى مشروع Laravel جاهز. إذا كنت تستخدم Composer، فالأمر بسيط جدًا:
composer create-project laravel/laravel laravel-api-example
بعد ذلك ادخل إلى مجلد المشروع:
cd laravel-api-example
ثم شغّل الخادم المحلي:
php artisan serve
عادةً ستجد التطبيق يعمل على:
http://127.0.0.1:8000
الخطوة التالية هي إعداد قاعدة البيانات في ملف .env. سنفترض أنك تستخدم MySQL:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_api
DB_USERNAME=root
DB_PASSWORD=
ثم أنشئ قاعدة البيانات من خلال phpMyAdmin أو من سطر الأوامر، وبعدها تأكد من أن الاتصال يعمل بشكل جيد عبر:
php artisan migrate
إذا نجحت عملية الهجرة الافتراضية، فهذا يعني أن البيئة الأساسية أصبحت جاهزة.
اختيار هيكلة مناسبة للمشروع
من الأخطاء الشائعة في APIs الصغيرة أنها تبدأ مرتبة، ثم تتحول مع الوقت إلى كود متشابك يصعب فهمه. لهذا من المفيد أن تفكر منذ البداية في هيكلة واضحة. Laravel يوفّر لك كل شيء تقريبًا، لكن قرار التنظيم ما زال مسؤوليتك كمطور.
في هذا المقال سنفترض أننا نبني API لإدارة posts، وسنستخدم العناصر التالية:
Model:
PostMigration: لإنشاء جدول posts
Controller:
PostControllerResource:
PostResourceRequests:
StorePostRequestوUpdatePostRequestAuth: باستخدام Sanctum لاحقًا
هذا التقسيم ليس ترفًا. هو الذي يحميك عندما يكبر المشروع. فالموديل يتعامل مع البيانات والعلاقات، والكنترولر يتعامل مع الطلبات، والـ resource يتعامل مع شكل الاستجابة، والـ request يتعامل مع التحقق من صحة المدخلات. عندما تضع كل جزء في مكانه الصحيح، يصبح التطوير أسرع، والاختبار أسهل، والتعديل أقل إيلامًا.
إنشاء نموذج وجدول قاعدة البيانات
لنبدأ بإنشاء الـ model مع migration:
php artisan make:model Post -m
سيُنشئ Laravel ملف model وملف migration. افتح ملف migration الذي تم إنشاؤه داخل database/migrations وعدّله ليكون كالتالي:
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->boolean('is_published')->default(false);
$table->timestamps();
});
}
ثم شغّل الهجرة:
php artisan migrate
الآن ننتقل إلى model Post. افتح الملف داخل app/Models/Post.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title',
'slug',
'content',
'is_published',
];
protected $casts = [
'is_published' => 'boolean',
];
}
هذا الجزء مهم جدًا. fillable يحميك من مشاكل mass assignment، وcasts يجعل قيمة is_published تُعامل كقيمة منطقية بدلًا من نص عادي. هذه التفاصيل الصغيرة هي ما يجعل الكود أكثر ثباتًا عندما يبدأ عدد الطلبات في الزيادة.
إنشاء Resource Controller
الآن سننشئ الكنترولر:
php artisan make:controller Api/PostController --api
استخدام --api يعني أن Laravel ينشئ لك methods مناسبة لـ API فقط، مثل index, store, show, update, destroy، دون create وedit اللتين تخصان الواجهات التقليدية.
افتح الكنترولر داخل app/Http/Controllers/Api/PostController.php، وسنبدأ بتنظيمه تدريجيًا.
قبل أن نكتب أي منطق، نحتاج إلى Resource لتهيئة شكل البيانات في الاستجابة. أنشئه بالأمر:
php artisan make:resource PostResource
ثم عدّل الملف app/Http/Resources/PostResource.php ليصبح:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'content' => $this->content,
'is_published' => $this->is_published,
'created_at' => $this->created_at?->toDateTimeString(),
'updated_at' => $this->updated_at?->toDateTimeString(),
];
}
}
الـ Resource هنا يحميك من مشكلة شائعة جدًا: إرسال كامل model كما هو. ذلك قد يسبب تسريب حقول لا تحتاجها الواجهة، أو يجعل شكل الـ JSON غير ثابت. باستخدام Resource أنت تتحكم في شكل الاستجابة وتمنح العميل API نظيفًا ومتوقعًا.
تعريف المسارات Routes
الآن ننتقل إلى routes/api.php. هذا الملف مخصص لمسارات API في Laravel. أضف التالي:
use App\Http\Controllers\Api\PostController;
use Illuminate\Support\Facades\Route;
Route::apiResource('posts', PostController::class);
هذا السطر وحده ينشئ لك مجموعة من المسارات القياسية:
GET /api/postsPOST /api/postsGET /api/posts/{post}PUT /api/posts/{post}PATCH /api/posts/{post}DELETE /api/posts/{post}
ميزة apiResource أنها تختصر كثيرًا، لكنها أيضًا تحافظ على معايير REST وتمنحك بنية واضحة. وهنا تبدأ الفكرة تتبلور: بدل أن تكتب عشرات المسارات يدويًا، أنت تقول للـ framework: هذه موارد، ونمط التعامل معها معروف.
بناء أول endpoint: جلب كل المقالات
لنكتب الآن method index داخل PostController:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
$posts = Post::latest()->paginate(10);
return PostResource::collection($posts);
}
}
هذا المثال بسيط لكنه عملي جدًا. استخدمنا latest() لترتيب المقالات من الأحدث إلى الأقدم، ثم paginate(10) لعرض 10 عناصر فقط في كل صفحة. ثم أعدنا النتيجة عبر PostResource::collection() حتى نحافظ على شكل JSON موحد.
عند استهلاك هذا endpoint من الواجهة الأمامية، ستحصل على بيانات منظمة مع روابط pagination وmetadata، وهذا مهم عندما يبدأ عدد السجلات في التزايد. لا أحد يحب أن يحمّل 50,000 سجل دفعة واحدة ثم يشتكي من البطء.
إنشاء endpoint لعرض مقال واحد
المقال الفردي يُعرض عادة عبر show:
public function show(Post $post)
{
return new PostResource($post);
}
هنا استخدمنا route model binding. بدل أن نبحث يدويًا عن المقال عبر findOrFail, Laravel يحقن لنا الـ model مباشرة طالما أن الـ route يحتوي على post. هذا يجعل الكود أقصر وأكثر وضوحًا.
ومع ذلك، من الجيد أن تفهم الفكرة تحت السطح. عندما يزور المستخدم:
GET /api/posts/1
يقوم Laravel بجلب المقال رقم 1 تلقائيًا، وإذا لم يجده يعيد خطأ 404 بشكل افتراضي. هذا السلوك رائع لأنك لا تحتاج إلى إعادة اختراع العجلة في كل مرة.
التحقق من البيانات Validation
الآن نصل إلى جزء مهم جدًا. لا يجب أبدًا أن يعتمد API على الثقة العمياء في البيانات القادمة من العميل. حتى لو كانت الواجهة الأمامية من نفس الفريق، فالتحقق من البيانات يجب أن يكون موجودًا في الخلفية دائمًا. هنا نستخدم Form Request بدلًا من كتابة validation مباشرة داخل controller.
أنشئ طلب إنشاء مقال:
php artisan make:request StorePostRequest
ثم افتح الملف app/Http/Requests/StorePostRequest.php واكتب:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'slug' => ['required', 'string', 'max:255', 'unique:posts,slug'],
'content' => ['required', 'string'],
'is_published' => ['sometimes', 'boolean'],
];
}
}
ثم أنشئ طلب التحديث:
php artisan make:request UpdatePostRequest
واكتبه هكذا:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdatePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$postId = $this->route('post')?->id;
return [
'title' => ['sometimes', 'required', 'string', 'max:255'],
'slug' => [
'sometimes',
'required',
'string',
'max:255',
Rule::unique('posts', 'slug')->ignore($postId),
],
'content' => ['sometimes', 'required', 'string'],
'is_published' => ['sometimes', 'boolean'],
];
}
}
في هذا التحقق نلاحظ شيئًا مهمًا: عند التحديث، لا نريد أن يتعطل حفظ المقال لأن slug نفسه موجود أصلًا لهذا السجل. لهذا استخدمنا ignore($postId). هذه التفاصيل تبدو صغيرة، لكنها تمنع أخطاء مزعجة جدًا في المشاريع الحقيقية.
إنشاء مقال جديد
الآن نعود إلى PostController ونضيف method store:
use App\Http\Requests\StorePostRequest;
use Illuminate\Support\Str;
public function store(StorePostRequest $request)
{
$validated = $request->validated();
$post = Post::create([
'title' => $validated['title'],
'slug' => $validated['slug'],
'content' => $validated['content'],
'is_published' => $validated['is_published'] ?? false,
]);
return new PostResource($post);
}
هذه الطريقة واضحة ومباشرة. نحن نأخذ البيانات الموثوقة فقط من validated() ثم ننشئ السجل. أحيانًا يميل بعض المطورين إلى استعمال $request->all() لأنه أسهل، لكن هذا سلوك خطر. إن كان لديك 50 حقلًا في الطلب، فأنت لا تريد أن تسمح بمرور أي شيء منها فقط لأنك نسيت فلترة واحد منها.
يمكنك أيضًا أن تولد slug تلقائيًا إذا لم يكن موجودًا. مثلًا:
use Illuminate\Support\Str;
$slug = Str::slug($validated['title']);
لكن بما أننا صممنا الواجهة لتُرسل slug صراحة، فلن نُعقد المثال أكثر من اللازم. وفي المشاريع العملية، أحيانًا يكون إنشاء slug تلقائيًا أفضل، وأحيانًا يكون طلبه من العميل أفضل بحسب منطق العمل.
تحديث مقال موجود
أضف method update:
use App\Http\Requests\UpdatePostRequest;
public function update(UpdatePostRequest $request, Post $post)
{
$validated = $request->validated();
$post->update($validated);
return new PostResource($post->fresh());
}
هنا استخدمنا fresh() بعد التحديث لضمان إعادة البيانات الأحدث من قاعدة البيانات. في أغلب الأحيان سيكون update() كافيًا، لكن fresh() يريحك عندما تكون هناك تغييرات أخرى قد تحصل عبر observer أو mutator أو logic إضافي.
عند التحديث الجزئي، يسمح لنا sometimes في الـ request بأن نرسل فقط الحقول التي نريد تعديلها. هذا ممتاز لأنه يجعل الـ API مرنًا، كما أنه ينسجم مع فكرة PATCH و PUT بحسب السيناريو الذي تعتمد عليه. بعض الفرق تفضل استعمال PUT للتحديث الكامل، وبعضها تكتفي بمرونة الجزئي. المهم أن يكون السلوك متسقًا ومفهومًا.
حذف مقال
الـ delete method بسيط جدًا:
public function destroy(Post $post)
{
$post->delete();
return response()->json([
'message' => 'Post deleted successfully.',
]);
}
يمكنك أيضًا إعادة 204 No Content بدل JSON، وهذا خيار شائع في REST APIs:
public function destroy(Post $post)
{
$post->delete();
return response()->noContent();
}
كلا الأسلوبين صحيح، لكن noContent() أنظف عندما لا تحتاج إلى أي رسالة إضافية.
تحسين شكل الاستجابات
الآن بعد أن أنشأنا CRUD الأساسي، نحتاج إلى التفكير قليلًا في تجربة المطور الذي سيستهلك API. من السهل جدًا أن تكتب endpoint يعمل، لكن من الصعب أن تكتب API مريحًا. الراحة هنا تعني أن الاستجابات واضحة، والأخطاء مفهومة، والحقول ثابتة، والـ status codes منطقية.
في Laravel يمكنك تخصيص الاستجابة بحيث تعيد بيانات وميتا داتا إضافية عند الحاجة:
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return response()->json([
'message' => 'Post created successfully.',
'data' => new PostResource($post),
], 201);
}
هذا النوع من الاستجابة ممتاز عندما تريد أن تعرض رسالة مناسبة للواجهة الأمامية بالإضافة إلى البيانات نفسها. لكن انتبه إلى أن الإفراط في التنوع في الاستجابة يسبب فوضى. اختر نمطًا واحدًا وطبّقه في كامل المشروع.
التعامل مع الأخطاء بطريقة احترافية
الأخطاء جزء طبيعي من أي API، والمشكلة ليست في وجود الخطأ، بل في نوعية الرسالة التي يتلقاها العميل. من غير الجيد أن يعود الـ API برسالة عامة أو HTML أو stack trace في بيئة الإنتاج. يجب أن تكون الأخطاء JSON واضحة.
Laravel يعالج كثيرًا من هذا تلقائيًا، مثل أخطاء validation التي تعود عادة بشكل منظم. لكن من المفيد أن تفهم كيف تبني طبقة error handling جيدة. مثلًا، إذا لم يتم العثور على مورد، فإن Laravel يعيد 404. وإذا فشل التحقق، يعيد 422. وإذا لم يكن هناك إذن، يعيد 403.
يمكنك أيضًا تخصيص الاستجابة في app/Exceptions/Handler.php عندما تحتاج إلى نمط موحد:
use Illuminate\Http\Exceptions\HttpResponseException;
use Throwable;
public function render($request, Throwable $e)
{
if ($request->expectsJson()) {
return response()->json([
'message' => 'Something went wrong.',
], 500);
}
return parent::render($request, $e);
}
لكن هنا يجب الحذر. لا تجعل كل الأخطاء تعود بنفس الرسالة العامة، لأن هذا يضعف تجربة التصحيح debugging. الأفضل أن تحافظ على توازن: رسائل مفهومة للمستهلك، وتسجيل داخلي مناسب للفريق.
إضافة البحث والفلترة
في التطبيقات الحقيقية، قلّما يكفي أن تعرض كل المقالات كما هي. المستخدم غالبًا يريد البحث، أو التصفية حسب حالة النشر، أو الترتيب حسب الأحدث. لهذا سنضيف بعض المنطق داخل index.
public function index(Request $request)
{
$query = Post::query();
if ($request->filled('search')) {
$query->where('title', 'like', '%' . $request->search . '%');
}
if ($request->has('is_published')) {
$query->where('is_published', $request->boolean('is_published'));
}
$posts = $query->latest()->paginate(10);
return PostResource::collection($posts);
}
بهذه الطريقة يستطيع العميل أن يطلب:
GET /api/posts?search=Laravel&is_published=1
وهذا يفتح الباب لواجهات أكثر ذكاءً. بدل أن تكون API مجرد مستودع بيانات، تصبح أداة مرنة تساعد الواجهة الأمامية على تقديم تجربة استخدام أفضل.
إضافة علاقات بين النماذج
غالبًا لن يبقى الـ API مقتصرًا على جدول واحد. قد تحتاج إلى comments مرتبطة بـ posts, أو categories, أو users. لنفترض أننا نريد ربط المقال بالمستخدم الذي أنشأه.
أضف حقلًا في migration:
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
ثم أضف العلاقة في Post:
public function user()
{
return $this->belongsTo(User::class);
}
وفي User:
public function posts()
{
return $this->hasMany(Post::class);
}
عندما تصبح العلاقات جزءًا من API، أنت بحاجة إلى التفكير في eager loading لتجنب N+1 queries. مثلًا:
$posts = Post::with('user')->latest()->paginate(10);
ثم يمكنك توسيع PostResource:
'user' => [
'id' => $this->whenLoaded('user')?->id,
'name' => $this->whenLoaded('user')?->name,
],
أو بشكل أوضح:
'user' => new UserResource($this->whenLoaded('user')),
وهكذا تحافظ على الأداء وتمنع تحميل العلاقات بشكل عشوائي.
المصادقة Authentication باستخدام Laravel Sanctum
معظم الـ APIs الحقيقية لا تكون مفتوحة بالكامل. ستحتاج إلى تسجيل دخول، وتوليد tokens، والتحكم في الوصول. في Laravel، أحد أسهل الحلول وأفضلها للـ SPA والتطبيقات الحديثة هو Sanctum.
تثبيته يكون عادة عبر:
composer require laravel/sanctum
php artisan sanctum:install
php artisan migrate
ثم تأكد من إعداد الـ middleware المناسب في bootstrap/app.php أو حسب نسخة Laravel التي تستخدمها، لأن طريقة التسجيل تختلف قليلًا بين الإصدارات الحديثة. المهم أن Sanctum يتيح لك إنشاء tokens للمستخدمين بسهولة.
مثال على تسجيل الدخول وإرجاع token:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Models\User;
public 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' => 'Invalid credentials.',
], 401);
}
$token = $user->createToken('api-token')->plainTextToken;
return response()->json([
'token' => $token,
'user' => $user,
]);
}
وبعدها يمكنك حماية المسارات:
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', PostController::class);
});
الآن لن يستطيع أي مستخدم غير مصادق الوصول إلى هذه المسارات إلا إذا كان يملك token صالحًا. هذا النوع من الحماية مهم جدًا إذا كان الـ API يتعامل مع بيانات خاصة أو عمليات تعديل.
صلاحيات الوصول Authorization
المصادقة تعني أننا نعرف من المستخدم، أما الصلاحيات فتجيب عن سؤال: ماذا يُسمح له أن يفعل؟ في المشاريع الواقعية، ليس كل مستخدم يستطيع تعديل أي مقال. ربما يستطيع فقط تعديل مقالاته هو.
يمكنك استخدام Policies لهذا الغرض. أنشئ policy:
php artisan make:policy PostPolicy --model=Post
ثم داخل PostPolicy:
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
ثم في الكنترولر:
public function update(UpdatePostRequest $request, Post $post)
{
$this->authorize('update', $post);
$post->update($request->validated());
return new PostResource($post->fresh());
}
بهذا الشكل تتحول الحماية من مجرد "وجود login" إلى "وجود login مع الحق المناسب". وهذه خطوة أساسية في أي API محترف.
تنظيم الـ API Versioning
مع الوقت، يتغير الـ API. ربما اليوم تبني v1، وبعد عام تحتاج v2 دون كسر التطبيقات القديمة. هنا تأتي فكرة versioning. يمكنك تنظيم المسارات هكذا:
Route::prefix('v1')->group(function () {
Route::apiResource('posts', PostController::class);
});
ثم لاحقًا يمكنك إنشاء V2\PostController. هذه الاستراتيجية تحميك من ألم التغييرات الجذرية. أحيانًا يكفي أن تضيف حقلًا جديدًا، وأحيانًا تغير شكل الاستجابة بالكامل. versioning الجيد يعني أنك تستطيع التطوير دون أن تكسر عملاءك الحاليين.
توثيق API
إذا كان المشروع لا يعمل إلا في رأسك، فهو ليس API جيدًا بما يكفي. التوثيق جزء من التجربة. يمكنك كتابة وثائق يدوية في Markdown، أو استخدام أدوات مثل OpenAPI/Swagger. حتى لو كنت تعمل وحدك، التوثيق يوفر وقتًا ضخمًا عندما تعود للمشروع بعد شهرين وتنسى لماذا كنت قد بنيت endpoint بطريقة معينة.
مثال بسيط على وصف endpoint داخل الوثائق:
GET /api/posts
Query Parameters:
- search: string
- is_published: boolean
- page: integer
Response 200:
{
"data": [...],
"links": {...},
"meta": {...}
}
الوثائق لا يجب أن تكون أكاديمية أو معقدة. يكفي أن تكون واضحة وصادقة ومحدثة. والأسهل دائمًا هو أن تكتبها وأنت ما زلت تفهم المنطق، لا بعد أن ينساه الجميع.
كتابة اختبارات API
الاختبارات ليست رفاهية، خصوصًا في APIs التي تتوسع مع الوقت. كل endpoint جديد قد يكسر شيئًا قديمًا دون أن تلاحظ. لهذا يجب أن تختبر الأساسيات.
أنشئ اختبارًا:
php artisan make:test PostApiTest
ثم اكتب مثالًا بسيطًا:
<?php
namespace Tests\Feature;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostApiTest extends TestCase
{
use RefreshDatabase;
public function test_can_get_posts_list(): void
{
Post::factory()->count(3)->create();
$response = $this->getJson('/api/posts');
$response->assertStatus(200);
$response->assertJsonStructure([
'data',
'links',
'meta',
]);
}
public function test_authenticated_user_can_create_post(): void
{
$user = User::factory()->create();
$payload = [
'title' => 'My First Post',
'slug' => 'my-first-post',
'content' => 'This is the content.',
'is_published' => true,
];
$response = $this->actingAs($user)->postJson('/api/posts', $payload);
$response->assertStatus(201);
$response->assertJsonFragment([
'title' => 'My First Post',
]);
}
}
الاختبارات هنا تمنحك ثقة كبيرة. بدل أن تعود يدويًا لتجربة كل endpoint في كل مرة، يمكنك تشغيل الاختبارات والتأكد أن الـ API ما زال يعمل كما ينبغي.
استخدام Factories وSeeders
حين تبني API وتحتاج إلى بيانات تجريبية، تصبح factories وseeders مفيدة جدًا. أنشئ factory:
php artisan make:factory PostFactory --model=Post
ثم داخل factory:
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class PostFactory extends Factory
{
public function definition(): array
{
$title = $this->faker->sentence(6);
return [
'title' => $title,
'slug' => Str::slug($title) . '-' . $this->faker->unique()->numberBetween(1, 9999),
'content' => $this->faker->paragraphs(5, true),
'is_published' => $this->faker->boolean(),
];
}
}
ثم في seeder:
use App\Models\Post;
Post::factory()->count(50)->create();
هذا يساعدك في الاختبار اليدوي، وفي تطوير الواجهة الأمامية، وفي تجربة pagination والبحث والتصفية ببيانات أقرب للواقع.
تحسين الأداء
بمجرد أن يكبر الـ API، يبدأ الأداء في الظهور كموضوع مهم. كثير من مشاكل الأداء لا تأتي من Laravel نفسه، بل من طريقة كتابة الاستعلامات. استخدام with() للعلاقات، والتقليل من الاستعلامات داخل الحلقات، وإضافة pagination بدل جلب كل البيانات، كلها أمور تصنع فرقًا واضحًا.
مثلًا، بدل:
$posts = Post::all();
استخدم:
$posts = Post::latest()->paginate(10);
وبدل استدعاء العلاقة داخل loop بشكل يسبب N+1:
foreach ($posts as $post) {
echo $post->user->name;
}
تأكد من التحميل المسبق:
$posts = Post::with('user')->latest()->paginate(10);
كذلك يمكن استخدام caching في بعض الحالات، خاصة للبيانات التي لا تتغير كثيرًا مثل الإعدادات العامة أو القوائم الثابتة. لكن لا تدخل cache مبكرًا إلا عندما ترى حاجة حقيقية، لأن التعقيد الزائد قد يضر أكثر مما ينفع.
التعامل مع صياغة JSON بشكل موحد
من الجيد أن يعتمد API على شكل استجابة موحد. مثلًا، قد تقرر أن كل الاستجابات الناجحة تحتوي على:
{
"message": "",
"data": {}
}
وأن الأخطاء تحتوي على:
{
"message": "",
"errors": {}
}
هذا النمط يسهّل على مطور الواجهة الأمامية فهم ما سيحدث في كل مرة. لكن تذكر أن Laravel نفسه يعيد بعض الاستجابات الافتراضية مثل pagination structure وvalidation errors بشكل معين. يمكنك التعايش مع الشكل الافتراضي أو توحيده عبر طبقة خاصة بك. المهم أن لا يكون API متقلبًا. الاتساق هنا أهم من الجمال النظري.
أمثلة على استهلاك API من الواجهة الأمامية
حتى لو كان هذا المقال يركز على Laravel، من المفيد أن ترى كيف سيبدو استهلاك الـ API من جهة العميل. مثلًا باستخدام fetch:
fetch('/api/posts')
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error:', error);
});
ومع axios:
import axios from 'axios';
axios.get('/api/posts')
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(error.response?.data || error.message);
});
في الواقع، جزء كبير من نجاح الـ API لا يتعلق فقط بما يفعله الخادم، بل بمدى سهولة استهلاكه. عندما تكون الاستجابات واضحة والـ status codes منطقية والحقول ثابتة، يشعر مطور الواجهة الأمامية أنه يعمل مع API محترم، لا مع صندوق مفاجآت.
أخطاء شائعة يجب تجنبها
عند بناء REST API في Laravel، هناك أخطاء تتكرر كثيرًا عند المبتدئين، وأحيانًا حتى عند من لديهم خبرة لكنهم مستعجلون. من أبرز هذه الأخطاء إعادة request()->all() إلى قاعدة البيانات مباشرة دون validated(), أو ترك المسارات بدون versioning، أو وضع منطق الأعمال داخل controller بشكل ضخم، أو إهمال الـ Resources والاكتفاء بإرجاع model خام. كذلك يقع البعض في مشكلة إرجاع HTML بدل JSON عند الفشل، أو استعمال status code خاطئ، أو نسيان pagination، أو الاعتماد على queries ثقيلة بلا داعٍ.
خطأ آخر مهم جدًا هو تجاهل حدود الأمان. ليس كل endpoint يجب أن يكون عامًا، وليس كل مستخدم يجب أن يملك القدرة على تعديل كل سجل. في بعض المشاريع، مجرد نسيان auth:sanctum أو policy واحدة قد يفتح بابًا كبيرًا جدًا للمشاكل. لذلك تعامل مع الـ API كواجهة عامة حساسة، حتى لو كان المشروع صغيرًا في بدايته.
مثال كامل مبسط للكنترولر
بعد كل هذا الشرح، قد يكون من المفيد أن ترى صورة كاملة مختصرة لـ PostController:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
$query = Post::query();
if ($request->filled('search')) {
$query->where('title', 'like', '%' . $request->search . '%');
}
if ($request->has('is_published')) {
$query->where('is_published', $request->boolean('is_published'));
}
$posts = $query->latest()->paginate(10);
return PostResource::collection($posts);
}
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return (new PostResource($post))
->response()
->setStatusCode(201);
}
public function show(Post $post)
{
return new PostResource($post);
}
public function update(UpdatePostRequest $request, Post $post)
{
$post->update($request->validated());
return new PostResource($post->fresh());
}
public function destroy(Post $post)
{
$post->delete();
return response()->noContent();
}
}
هذا المثال ليس مجرد كود، بل هو خلاصة التنظيم الذي تحدثنا عنه. عندما تقرأه لاحقًا، ستلاحظ أن كل شيء في مكانه: الـ controller لا يبالغ، الـ request يتكفل بالتحقق، الـ resource يتكفل بالشكل، والـ model يحتفظ بمنطق البيانات الأساسي.
كيف تفكر كمطور API محترف؟
الفرق الحقيقي بين API يعمل وAPI جيد لا يكمن فقط في عدد الأسطر أو في طريقة كتابة الكود، بل في طريقة التفكير. المطور الجيد يسأل نفسه: هل الاستجابة مفهومة؟ هل الأخطاء واضحة؟ هل يمكن توسيع هذا endpoint لاحقًا دون كسر القديم؟ هل يوجد فصل واضح بين المسؤوليات؟ هل التحقق من البيانات موجود في المكان الصحيح؟ هل الأداء مقبول؟ هل يمكن لفرد آخر في الفريق فهم هذا الكود بعد أسبوعين من الآن؟
حين تلتزم بهذه الأسئلة، ستلاحظ أن Laravel يصبح أداة قوية جدًا، لا مجرد إطار عمل. هو يسهّل عليك بناء APIs بسرعة، لكنه في الوقت نفسه يسمح لك أن تحافظ على مستوى احترافي من التنظيم والنظافة.
خاتمة عملية
بناء REST API باستخدام Laravel ليس أمرًا معقدًا عندما تفهم الفكرة الأساسية وراء كل طبقة. أنت تبدأ من model وmigration، ثم تنشئ routes واضحة، ثم controllers خفيفة، ثم validation عبر Form Requests، ثم Resources لتوحيد الاستجابات، وبعد ذلك تضيف authentication وauthorization عندما تحتاج إليهما. وبعد أن يعمل كل شيء، لا تتوقف عند "المشروع يشتغل"، بل انتبه إلى الاختبارات، التوثيق، الأداء، والتوسعة المستقبلية.
الجميل في Laravel أنه يمنحك طريقًا مريحًا إلى حد كبير، لكن الاحتراف الحقيقي يظهر عندما تستخدم الأدوات بشكل منظم. لا تكن أسيرًا لفكرة "المهم أن يعمل". اجعل API يعمل بشكل جيد، وقابل للفهم، وسهل الصيانة، وآمن، وقابل للنمو. هذا بالضبط ما تحتاجه المشاريع الحقيقية، وهذا ما يجعل وقتك مع Laravel يستحق.
إذا بدأت اليوم بمشروع صغير مثل posts أو products, فلا تنظر إليه كمجرد تدريب. انظر إليه كقاعدة ستبني فوقها لاحقًا نظامًا أكبر، وربما منصة كاملة. كل endpoint تنشئه بطريقة نظيفة اليوم سيُوفر عليك ساعات، وربما أيامًا، عندما يأتي وقت التوسعة أو التصحيح أو تسليم المشروع لفريق آخر.