اصول طراحی API های RESTful
مقدمه
این سند، اصول بنیادین و بهترین شیوههای طراحی API های RESTful در سیستم ما را تبیین میکند. به سوءتفاهمهای رایج درباره معماری REST میپردازد و راهنماییهای روشنی برای مدلسازی منابع، بهویژه برای عملیاتهای پیچیده کسبوکار فراتر از عملیاتهای پایه CRUD ارائه میدهد.
مفاهیم اصلی
منبع (Resource) چیست؟
یک منبع یک اسم است، نه یک فعل. منبع نمایانگر یک مفهوم یا موجودیت در دامنه کسبوکار ماست که میتواند شناسایی، نامگذاری، آدرسدهی و بین سرور و کلاینت منتقل شود.
مثالهای خوب: Cart (سبد خرید)، User (کاربر)، Payment (پرداخت)، Product (محصول)، Invoice (فاکتور)، Subscription (اشتراک)
مثالهای نامناسب: QuickPay (پرداخت سریع)، VerifyPayment (تأیید پرداخت)، AddItemToCart (افزودن آیتم به سبد خرید)
یک منبع لزوماً یک جدول پایگاه داده نیست. منابع در لایه API و کسبوکار وجود دارند، در حالی که جداول، جزئیات پیادهسازی در لایه داده هستند. این دغدغهها را از هم جدا نگه دارید.
زیرمنبع (Sub-resource) چیست؟
یک زیرمنبع، منبعی است که وجود آن تنها در بستر منبع دیگری معنادار است. این یک رابطه ترکیبی را نشان میدهد.
مثالها:
- یک
Comment(نظر) بدون یکPost(پست) بیمعناست. بنابراین،Commentیک زیرمنبعPostاست. - یک
CartItem(آیتم سبد خرید) نمیتواند بدون یکCart(سبد خرید) وجود داشته باشد. بنابراین،CartItemیک زیرمنبعCartاست.
این رابطه به شکلی زیبا در URI ها منعکس میشود:
GET /posts/{postId}/comments // دریافت تمام نظرات برای یک پست خاص
GET /carts/{cartId}/items // دریافت تمام آیتمهای یک سبد خرید خاص
سوءتفاهمهای رایج
"REST فقط برای عملیات CRUD است"
این یکی از بزرگترین سوءتفاهمها درباره REST است. REST یک سبک معماری است، نه یک پروتکل محدود به چهار عملیات اصلی.
در حالی که عملیات CRUD به طور طبیعی با متدهای HTTP (POST، GET، PUT/PATCH، DELETE) نگاشت میشوند، REST میتواند عملیاتهای پیچیده را از طریق مدلسازی صحیح منابع مدیریت کند.
اصل راهنما: به جای تفکر درباره "فعل"، به این فکر کنید که آن فعل چه "اسمی" را ایجاد میکند یا وضعیت کدام "اسم" را تغییر میدهد.
مدلسازی منابع برای عملیاتهای پیچیده
مثال: تأیید پرداخت
به جای:
POST /payment/verify
از این استفاده کنید:
POST /payments/{paymentId}/verifications
این رویکرد:
- یک سابقه حسابرسی از تلاشهای تأیید ایجاد میکند
- امکان عملیات یکتا (idempotency) را فراهم میکند (جلوگیری از پردازش تکراری)
- با ایجاد یک منبع جدید، به اصول REST پایبند است
مطالعات موردی
- پردازش پرداخت
- مدیریت سبد خرید
- عملیاتهای کوپن
پردازش پرداخت
مسئله
سیستم ما چندین جریان پرداخت دارد: QuickPay، JustPay و Verify که در حال حاضر به صورت زیر پیادهسازی شدهاند:
v1/client/payment/quickpayv1/client/payment/justpayv1/client/payment/verify
این نقاط پایانی، منطق کسبوکار را به لایه API نشت میدهند و باعث ایجاد وابستگی شدید بین کلاینت و سرور میشوند.
راهحل
QuickPay (ترکیب ایجاد سبد خرید و پرداخت):
به جای یک نقطه پایانی که دو کار انجام میدهد، دغدغهها را جدا کنید:
- کلاینت یک سبد خرید ایجاد میکند:
POST /carts - سرور با جزئیات سبد خرید و اقدامات ممکن پاسخ میدهد (HATEOAS)
- کلاینت پرداخت را آغاز میکند:
POST /paymentsبا{"cartId": "cart_123"}
JustPay (پرداخت برای منابع مختلف):
POST /payments
با بدنه درخواست که منبع را مشخص میکند:
// پرداخت برای یک سبد خرید
{ "sourceType": "cart", "sourceId": "cart_123" }
// پرداخت برای یک فاکتور
{ "sourceType": "invoice", "sourceId": "inv_456" }
Verify (تأیید پرداخت):
POST /payments/{paymentId}/verifications
این یک منبع تأیید ایجاد میکند که تلاش، نتیجه و هر داده مرتبط از بانک را ثبت میکند.
مدیریت سبد خرید
مسئله
نقاط پایانی فعلی مانند cart/addItem و cart/deleteItem از افعال در URI استفاده میکنند.
راهحل
افزودن یک آیتم:
POST /carts/{cartId}/items
حذف یک آیتم:
DELETE /carts/{cartId}/items/{itemId}
این نقاط پایانی به درستی آیتمهای سبد خرید را به عنوان زیرمنابع یک سبد خرید مدلسازی میکنند.
عملیاتهای کوپن
مسئله
نقاط پایانی مانند coupon/check، coupon/apply و coupon/unapply از افعال استفاده میکنند و به وضوح نشان نمیدهند که به چه چیزی اعمال میشوند.
راهحل
بررسی اعتبار کوپن:
POST /coupon-validations
بدنه شامل کد کوپن و اطلاعات سبد خرید است. پاسخ، اعتبار را نشان میدهد.
اعمال یک کوپن:
POST /carts/{cartId}/applied-coupons
یک رابطه بین سبد خرید و کوپن ایجاد میکند.
حذف یک کوپن:
DELETE /carts/{cartId}/applied-coupons/{couponCode}
رابطه بین سبد خرید و کوپن را حذف میکند.
ملاحظات معماری
هماهنگسازی سمت سرور
لایه هماهنگسازی برای فرآیندهای کسبوکار باید در سمت سرور قرار داشته باشد، نه کلاینت.
مزایا:
- امنیت: منطق کسبوکار در سرور محافظت میشود
- سادگی: کلاینتها "ساده" و سبک باقی میمانند
- یکپارچگی: قوانین کسبوکار به طور یکسان اعمال میشوند
- تکاملپذیری: فرآیندهای کسبوکار میتوانند بدون بهروزرسانی کلاینت تغییر کنند
الگوهای طراحی API
API های مبتنی بر منبع در مقابل API های مبتنی بر فرآیند
- مبتنی بر منبع (REST خالص)
- مبتنی بر فرآیند
هدف: ساخت یک API انعطافپذیر و قابل استفاده مجدد که میتواند توسط کلاینتهای مختلف به روشهای گوناگون مصرف شود.
رویکرد:
- API منابع پایدار (اسامی) را ارائه میدهد: سبدهای خرید، پرداختها، کاربران
- هماهنگسازی توسط کلاینت انجام میشود
- کلاینت توالی عملیات را میداند
چه زمانی استفاده کنیم: هنگام ساخت یک پلتفرم که کلاینتها تصمیم میگیرند چگونه از بلوکهای سازنده استفاده کنند.
هدف: پنهان کردن جزئیات پیادهسازی و منطق کسبوکار از کلاینتها.
رویکرد:
- API نقاط پایانی سطح بالا را ارائه میدهد که نمایانگر موارد استفاده کامل هستند
- سرور تمام هماهنگسازی را داخلی انجام میدهد
- کلاینت به سادگی فرآیند را با دادههای مورد نیاز فراخوانی میکند
چه زمانی استفاده کنیم: زمانی که کلاینتها باید از منطق کسبوکار و جزئیات پیادهسازی جدا باشند.
راهنمای پیادهسازی
قراردادهای نامگذاری
- از اسامی جمع برای منابع مجموعه استفاده کنید:
/users،/orders - از نامهای مشخص و خاص دامنه استفاده کنید:
/productsبه جای/items - از kebab-case برای منابع چند کلمهای استفاده کنید:
/shipping-addresses - از افعال در URI ها خودداری کنید مگر برای منابع کنترلر خاص
متدهای HTTP
| متد | هدف | مثال |
|---|---|---|
| GET | بازیابی منابع | GET /products |
| POST | ایجاد یک منبع جدید | POST /orders |
| PUT | جایگزینی کامل یک منبع | PUT /users/123 |
| PATCH | بهروزرسانی جزئی یک منبع | PATCH /products/456 |
| DELETE | حذف یک منبع | DELETE /carts/789 |
کدهای وضعیت
| کد | معنی | زمان استفاده |
|---|---|---|
| 200 | OK | GET، PUT، PATCH یا DELETE موفق |
| 201 | Created | POST موفق که یک منبع ایجاد کرده است |
| 204 | No Content | عملیات موفق بدون بدنه پاسخ |
| 400 | Bad Request | ورودی نامعتبر، خطاهای اعتبارسنجی |
| 401 | Unauthorized | احراز هویت مورد نیاز است |
| 403 | Forbidden | احراز هویت موفق بوده اما مجوز کافی نیست |
| 404 | Not Found | منبع وجود ندارد |
| 409 | Conflict | درخواست با وضعیت فعلی در تضاد است |
| 422 | Unprocessable Entity | خطاهای اعتبارسنجی معنایی |
| 500 | Internal Server Error | خطای سمت سرور |
الگوهای پیشرفته
HATEOAS (هایپرمدیا به عنوان موتور وضعیت برنامه)
پیوندهایی به منابع مرتبط و اقدامات ممکن در پاسخها قرار دهید:
{
"id": "cart_123",
"items": [],
"totalPrice": 0,
"_links": {
"self": { "href": "/carts/cart_123" },
"items": { "href": "/carts/cart_123/items" },
"checkout": { "href": "/payments", "method": "POST" }
}
}
عملیات یکتا (Idempotency)
برای عملیاتهایی که نباید به طور تصادفی تکرار شوند:
POST /payments
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000
سرور بررسی میکند که آیا درخواستی با این کلید قبلاً پردازش شده است یا خیر، قبل از پاسخ دادن.
ملاحظات معماری
جداسازی دغدغهها
یک API با طراحی مناسب باید جداسازی روشنی بین موارد زیر داشته باشد:
- کلاینت (Jupiter): نمایش و تعامل کاربر
- لایه API: نمایش منابع و معنیشناسی HTTP
- لایه سرویس (SUN): منطق کسبوکار، هماهنگسازی و قوانین دامنه
- لایه داده: ذخیرهسازی و بازیابی
این جداسازی تضمین میکند:
- منطق کسبوکار در سمت سرور باقی میماند
- کلاینتها "ساده" و سبک باقی میمانند
- API ثابت میماند در حالی که پیادهسازی میتواند تکامل یابد
- امنیت به درستی در هر لایه اعمال میشود
مدیریت فرآیندهای پیچیده کسبوکار
فرآیندهای پیچیده کسبوکار مانند QuickPay باید:
- در لایه سرویس پیادهسازی شوند
- از طریق نقاط پایانی منبعمحور ارائه شوند
- در سمت سرور هماهنگ شوند، نه در کلاینت
این رویکرد پیچیدگی کلاینت را کاهش میدهد، امنیت را بهبود میبخشد و جداسازی تمیز دغدغهها را حفظ میکند.
چارچوب تصمیمگیری برای رویکرد طراحی API
به طور پیشفرض از APIهای مبتنی بر منبع (REST خالص) استفاده کنید مگر اینکه دلیل قانعکنندهای برای استفاده از APIهای مبتنی بر فرآیند وجود داشته باشد.
از چکلیست زیر برای تعیین مناسب بودن API مبتنی بر فرآیند برای یک مورد استفاده خاص استفاده کنید:
-
نیاز به اتمیسیتی
- آیا عملیات نیاز به اجرای اتمیک به عنوان یک تراکنش واحد دارد؟
- آیا تقسیم آن به چندین فراخوانی API خطر ایجاد وضعیت ناسازگار را افزایش میدهد؟
-
پیچیدگی کلاینت
- آیا پیادهسازی این مورد با استفاده از APIهای REST خالص، کد کلاینت را بیش از حد پیچیده میکند؟
- آیا کلاینت نیاز به درک بیش از حد از فرآیند کسبوکار خواهد داشت؟
-
کارایی شبکه
- آیا پیادهسازی REST خالص نیاز به چندین رفت و برگشت دارد که به طور قابل توجهی بر عملکرد تأثیر میگذارد؟
- آیا این عملیات به طور مکرر در محیطهای با تأخیر بالا استفاده میشود؟
-
ملاحظات امنیتی
- آیا عملیات شامل قوانین کسبوکار حساسی است که نباید به کلاینت افشا شود؟
- آیا افشای مراحل جداگانه، آسیبپذیریهای امنیتی بالقوه ایجاد میکند؟
-
قابلیتهای کلاینت
- آیا کلاینت در توانایی خود برای هماهنگسازی جریانهای پیچیده محدود است (مانند دستگاههای IoT، اپلیکیشنهای موبایل ساده)؟
اگر به یک یا چند مورد از این سؤالات پاسخ "بله" میدهید، یک API مبتنی بر فرآیند ممکن است مناسب باشد. با این حال، حتی هنگام استفاده از APIهای مبتنی بر فرآیند:
- نقطه پایانی باید همچنان یک منبع (اسم) ایجاد یا اصلاح کند
- URI باید منعکسکننده منبعی باشد که ایجاد یا اصلاح میشود
- از متدهای استاندارد HTTP و کدهای وضعیت استفاده شود
- پاسخ باید شامل پیوندهایی به منابع مرتبط باشد (HATEOAS)
استراتژی مهاجرت
هرگز نقاط پایانی API موجود که کلاینتها به آنها وابسته هستند را تغییر ندهید.
- نسخهبندی API خود: کار روی v4 را با اصول جدید شروع کنید
- پیادهسازی تدریجی: با منابع سادهتر شروع کنید
- برنامه منسوخسازی: زمانبندی برای حذف تدریجی نسخههای قدیمیتر را اعلام کنید
- مستندسازی: راهنمای مهاجرت روشن برای کلاینتها ارائه دهید
نتیجهگیری
API های RESTful که به درستی طراحی شدهاند، به سیستمهای قابل نگهداریتر، مقیاسپذیرتر و امنتر منجر میشوند. با مدلسازی صحیح منابع و نگه داشتن منطق کسبوکار در سمت سرور، یک جداسازی تمیز از دغدغهها ایجاد میکنیم که هم به تیم توسعه و هم به مصرفکنندگان API سود میرساند.
به یاد داشته باشید: سرمایهگذاری اولیه در طراحی صحیح API، سود قابل توجهی در کاهش هزینههای نگهداری و افزایش انعطافپذیری سیستم در طول زمان به همراه دارد.