وقتی اپلیکیشن موبایل می‌خواهد سفارش‌های کاربر را از سایت بگیرد، یا افزونه‌ای می‌خواهد با یک سرویس بیرونی حرف بزند، یک قرارداد لازم است: به کدام آدرس چه بفرستم و چه چیزی تحویل می‌گیرم؟ REST API رایج‌ترین پاسخ به این پرسش است. هر چند اسمش پیچیده است، اصول آن ساده است و بیشتر دردسرها از نادیده‌گرفتن همین اصول می‌آید.

اگر سمت مرورگر را می‌خواهید، درخواست AJAX با fetch نشان می‌دهد چطور چنین API را از جاوااسکریپت صدا بزنید. این‌جا بیشتر سمت طراحی و سرور مهم است.

REST یعنی چه؟#

REST سبک طراحی است نه پروتکل یا کتابخانه. چند ایده‌ی اصلی دارد:

  • منبع (Resource): هر «چیز» مثل سفارش یا کاربر، آدرس خودش را دارد.
  • متد استاندارد HTTP: کاری که می‌خواهید روی منبع انجام شود با متد بیان می‌شود، نه با نام تابع در آدرس.
  • بدون حالت (Stateless): هر درخواست همه‌ی اطلاعات لازم برای فهمیده‌شدن را دارد؛ سرور حافظه‌ی گفت‌وگوی قبلی را نگه نمی‌دارد.
  • نمایش مشخص: معمولاً JSON، با هدر Content-Type درست.

آدرس‌ها: اسم، نه فعل#

آدرس باید منبع را نشان بدهد و کار را متد انجام دهد. مقایسه کنید:

ضعیفبهتر
GET /getOrdersGET /orders
POST /createOrderPOST /orders
POST /deleteOrder?id=5DELETE /orders/5
GET /orders/5/getItemsGET /orders/5/items

قاعده‌ها: اسم جمع، حروف کوچک، بدون فعل، و مسیر تودرتو فقط برای رابطه‌ی واقعی (اقلام یک سفارش).

متدها و کدهای وضعیت#

عملیاتمتدکد موفقنکته
فهرستGET /orders200صفحه‌بندی داشته باشد
یک موردGET /orders/5200در نبود: 404
ساختPOST /orders201آدرس مورد تازه را برگردانید
ویرایشPATCH /orders/5200فقط فیلدهای ارسالی
حذفDELETE /orders/5204بدنه ندارد

متد GET نباید چیزی را تغییر دهد. خطاها را هم با کد درست بفرستید: 400 یا 422 برای ورودی بد، 401 برای ناشناخته، 403 برای ممنوع، 404 برای نبودن، 429 برای درخواست بیش از حد و 500 برای خطای سرور. بازگرداندن 200 همراه با "error": true سردرگم‌کننده است.

قالب یکدست پاسخ و خطا#

یک قالب مشخص برای خطا انتخاب کنید و همه‌جا همان را بفرستید:

JSON
{
  "error": {
    "code": "validation_failed",
    "message": "داده‌ی ارسالی نامعتبر است.",
    "fields": {
      "phone": ["شماره‌ی تلفن معتبر نیست."]
    }
  }
}

فهرست‌ها را همیشه صفحه‌بندی کنید (?page=2&per_page=20) و سقف per_page را روی سرور بگذارید، وگرنه یک درخواست می‌تواند کل جدول را بخواهد.

فیلتر، مرتب‌سازی و جست‌وجو#

فیلتر و مرتب‌سازی را با پارامترهای آدرس بیان کنید، نه با آدرس‌های جدید:

Bash
GET /orders?status=paid&sort=-created_at&page=2&per_page=20

فقط فیلدهایی را برای فیلتر و مرتب‌سازی بپذیرید که از پیش فهرست کرده‌اید (فهرست سفید). اگر نام ستون را از ورودی مستقیم در کوئری بگذارید، راه حمله باز می‌شود. ستون‌هایی هم که فیلتر می‌شوند باید ایندکس داشته باشند؛ ایندکس در MySQL توضیح می‌دهد چرا.

نسخه‌بندی و سازگاری#

API پس از آن‌که کلاینتی به آن وصل شد، قراردادی است که نباید ناگهانی بشکند. حذف یا تغییر معنی یک فیلد، تغییر ناسازگار است؛ اضافه‌کردن فیلد تازه معمولاً بی‌خطر است. رایج‌ترین روش نسخه‌بندی، گذاشتن نسخه در مسیر است مثل /api/v1/orders.

احراز هویت و امنیت#

  • فقط روی HTTPS؛ توکن یا کلید را در آدرس نگذارید، چون در لاگ‌ها ثبت می‌شود. از هدر Authorization استفاده کنید.
  • احراز هویت و مجوز دو چیز جدا هستند: کاربر وارد شده ولی آیا اجازه‌ی دیدن سفارش شماره‌ی ۵ را دارد؟ این بررسی را برای هر منبع انجام دهید، نه فقط یک‌بار.
  • ورودی را اعتبارسنجی و پاک‌سازی کنید و کوئری‌ها را پارامتری بنویسید؛ امنیت کد PHP همین را مفصل می‌گوید.
  • محدودیت نرخ (Rate limit) بگذارید.
  • پیام خطا نباید جزئیات داخلی (مسیر فایل یا متن کوئری) را لو بدهد.

نمونه‌ی امن در وردپرس#

وردپرس REST API داخلی دارد. مهم‌ترین نکته، permission_callback است؛ بدون آن مسیر عمومی است:

PHP
<?php
add_action('rest_api_init', function () {
    register_rest_route('shop/v1', '/orders/(?P<id>\d+)', [
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'shop_get_order',
        'permission_callback' => function (WP_REST_Request $request) {
            return current_user_can('manage_woocommerce');
        },
        'args' => [
            'id' => [
                'validate_callback' => fn($v) => is_numeric($v),
                'sanitize_callback' => 'absint',
            ],
        ],
    ]);
});

function shop_get_order(WP_REST_Request $request) {
    $order = wc_get_order($request['id']);
    if (!$order) {
        return new WP_Error('not_found', 'سفارش پیدا نشد.', ['status' => 404]);
    }
    return rest_ensure_response([
        'id'     => $order->get_id(),
        'status' => $order->get_status(),
        'total'  => $order->get_total(),
    ]);
}

وردپرس هنگام فراخوانی از مرورگر برای درخواست‌های تغییردهنده، nonce را هم از طریق هدر X-WP-Nonce بررسی می‌کند. برای شروع افزونه‌نویسی، ساخت افزونه‌ی وردپرس از صفر را ببینید.

نمونه‌ی لاراول#

در لاراول، مسیرهای routes/api.php پیش‌فرض پیشوند /api دارند. یک کنترلر با اعتبارسنجی:

PHP
<?php
// routes/api.php
use App\Http\Controllers\OrderController;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('orders', OrderController::class)->only(['index', 'show', 'store']);
});

// app/Http/Controllers/OrderController.php
public function store(Request $request)
{
    $data = $request->validate([
        'product_id' => ['required', 'integer', 'exists:products,id'],
        'quantity'   => ['required', 'integer', 'min:1', 'max:100'],
    ]);

    $order = $request->user()->orders()->create($data);

    return response()->json($order, 201);
}

public function show(Request $request, Order $order)
{
    abort_unless($order->user_id === $request->user()->id, 403);

    return response()->json($order);
}

در show دقت کنید: بدون خط abort_unless، هر کاربر با تغییر عدد در آدرس سفارش دیگران را می‌بیند؛ این آسیب‌پذیری رایج «دسترسی ناامن به شیء» است.

خلاصه#

  • REST یعنی منبع با آدرس، کار با متد HTTP و پاسخ‌های بدون حالت.
  • آدرس‌ها اسم جمع باشند و فعل در آن‌ها نیاید.
  • از کد وضعیت درست و یک قالب یکدست خطا استفاده کنید.
  • فهرست‌ها صفحه‌بندی و سقف‌دار باشند و API نسخه داشته باشد.
  • احراز هویت کافی نیست؛ مجوز هر منبع را جدا بررسی کنید.
  • در وردپرس permission_callback و در لاراول Policy یا بررسی مالکیت را فراموش نکنید.