home / blog / تصميم RESTful API: القرارات التي تهم فعل...
هندسة البرمجيات Feb 08, 2026

تصميم RESTful API: القرارات التي تهم فعلاً

توقف عن الجدال حول الجمع مقابل المفرد. إليك قرارات تصميم API التي تؤثر فعلاً على تجربة المطور — الإصدارات، التصفح، صيغ الأخطاء، والتصفية.

تصميم RESTful API: القرارات التي تهم فعلاً

تصميم API هو تجربة مستخدم للمطورين

مستهلكو واجهة الـ API الخاصة بك هم مطورون. كل قرار تصميمي يؤثر على تجربتهم: مدى سرعة التكامل، وعدد المشكلات التي يواجهونها، وعدد تذاكر الدعم الفني التي يرسلونها. واجهة API المصممة جيدًا تعوّض تكلفتها من خلال تقليل عبء الدعم وتسريع التبني.

دعنا نركّز على القرارات التي تُحدث فرقًا حقيقيًا، وليس على النقاشات الهامشية.

استجابات أخطاء متّسقة

هذا هو أهم قرار تصميمي في واجهة API. عندما يحدث خطأ ما، يحتاج المطور إلى معرفة: ما الذي فشل، ولماذا فشل، وكيف يمكن إصلاحه.

// Bad: inconsistent error formats
{ "error": "Not found" }                           // string
{ "errors": { "email": "is required" } }            // object
{ "message": "Server error", "code": 500 }          // different shape
{ "success": false }                                // boolean with no info

// Good: one consistent format for ALL errors
{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "The given data was invalid.",
        "details": [
            {
                "field": "email",
                "message": "The email field is required."
            },
            {
                "field": "password",
                "message": "The password must be at least 8 characters."
            }
        ]
    }
}

// 404
{
    "error": {
        "code": "RESOURCE_NOT_FOUND",
        "message": "The requested invoice was not found.",
        "details": []
    }
}

// 500
{
    "error": {
        "code": "INTERNAL_ERROR",
        "message": "An unexpected error occurred. Please try again.",
        "details": []
    }
}

في Laravel، يمكنك توحيد هذا من خلال معالج الاستثناءات:

// app/Exceptions/Handler.php
class Handler extends ExceptionHandler
{
    public function render($request, Throwable $e)
    {
        if ($request->expectsJson()) {
            return $this->renderApiError($e);
        }

        return parent::render($request, $e);
    }

    private function renderApiError(Throwable $e): JsonResponse
    {
        return match(true) {
            $e instanceof ValidationException => response()->json([
                'error' => [
                    'code' => 'VALIDATION_ERROR',
                    'message' => $e->getMessage(),
                    'details' => collect($e->errors())->map(fn ($messages, $field) => [
                        'field' => $field,
                        'message' => $messages[0],
                    ])->values(),
                ],
            ], 422),

            $e instanceof ModelNotFoundException => response()->json([
                'error' => [
                    'code' => 'RESOURCE_NOT_FOUND',
                    'message' => 'The requested resource was not found.',
                    'details' => [],
                ],
            ], 404),

            default => response()->json([
                'error' => [
                    'code' => 'INTERNAL_ERROR',
                    'message' => app()->isProduction()
                        ? 'An unexpected error occurred.'
                        : $e->getMessage(),
                    'details' => [],
                ],
            ], 500),
        };
    }
}

الترقيم الصفحي: Cursor مقابل Offset

ترقيم Offset الصفحي مألوف لكنه يتعطل مع مجموعات البيانات الكبيرة (انزياح الصفحات عند إضافة أو حذف عناصر). ترقيم Cursor الصفحي أكثر موثوقية:

// Offset: simple but fragile
GET /api/posts?page=5&per_page=20

// Response includes pagination meta
{
    "data": [...],
    "meta": {
        "current_page": 5,
        "per_page": 20,
        "total": 1847,
        "last_page": 93
    },
    "links": {
        "next": "/api/posts?page=6&per_page=20",
        "prev": "/api/posts?page=4&per_page=20"
    }
}

// Cursor: consistent for real-time data
GET /api/posts?cursor=eyJpZCI6MTAwfQ&limit=20

// Laravel supports this natively
$posts = Post::orderBy('id')->cursorPaginate(20);
return PostResource::collection($posts);

التصفية والترتيب

نهج عملي قابل للتوسع:

// Filtering with query parameters
GET /api/orders?status=paid&customer_id=42&created_after=2026-01-01

// Sorting
GET /api/orders?sort=-created_at,total
// Prefix with - for descending

// In Laravel, a simple filter pipeline
class OrderController extends Controller
{
    public function index(Request $request)
    {
        $query = Order::query();

        $query->when($request->status, fn ($q, $status) =>
            $q->where('status', $status)
        );

        $query->when($request->customer_id, fn ($q, $id) =>
            $q->where('customer_id', $id)
        );

        $query->when($request->created_after, fn ($q, $date) =>
            $q->where('created_at', '>=', $date)
        );

        $query->when($request->sort, function ($q, $sort) {
            foreach (explode(',', $sort) as $field) {
                $direction = str_starts_with($field, '-') ? 'desc' : 'asc';
                $q->orderBy(ltrim($field, '-'), $direction);
            }
        });

        return OrderResource::collection($query->cursorPaginate(20));
    }
}

إدارة الإصدارات: أبقِها بسيطة

إدارة الإصدارات عبر عنوان URL هي الأكثر وضوحًا وسهولة في الفهم:

// URL prefix (recommended for most teams)
GET /api/v1/users
GET /api/v2/users

// In Laravel routes
Route::prefix('api/v1')->group(function () {
    Route::apiResource('users', V1\UserController::class);
});

Route::prefix('api/v2')->group(function () {
    Route::apiResource('users', V2\UserController::class);
});

نصائح لتصميم موارد API

  • استخدم أسماء الجمع للمجموعات: /api/users، وليس /api/user
  • استخدم التداخل عندما تكون الملكية واضحة: /api/users/42/orders لكن لا تتجاوز 3 مستويات من العمق
  • استخدم HTTP methods بشكل صحيح: GET للقراءة، POST للإنشاء، PUT/PATCH للتحديث، DELETE للحذف
  • أعِد الحالة 201 عند الإنشاء مع ترويسة Location
  • أعِد الحالة 204 عند الحذف (بدون محتوى)
  • أضمّن المورد المُنشأ أو المُحدَّث في الاستجابة لتوفير طلب إضافي على العميل
  • غلّف الاستجابات دائمًا بمفتاح data: { "data": { ... } } -- هذا يترك مجالًا للبيانات الوصفية

أفضل توثيق لواجهة API هو واجهة API لا تحتاج إلى توثيق. استخدم تسميات واضحة وأنماطًا متّسقة ورسائل خطأ مفيدة. اكتب التوثيق للأجزاء المعقدة، وليس للأشياء البديهية.

العودة لجميع المقالات