محدودیتهای REST
یک برنامهٔ وب باید منطق کسبوکار خود را بر مجموعهای از Entityها—برای مثال Product—و عملیات ممکن روی آنها—برای مثال بازیابی اطلاعات Product بر اساس شناسه—پیادهسازی کند. عملیات اصلی روی این Entityها باید حول چهار روش Create، Retrieve، Update و Delete که به اختصار CRUD نامیده میشوند طراحی شوند. این Entityها در REST «Resource» هستند و با قالبی مانند JSON، XML یا قالبهای دیگر نمایش داده میشوند.
بنابراین کلاینت با استفاده از HTTP، یا حتی پروتکلهای ارتباطی دیگری مانند gRPC یا SOAP، توابع CRUD روی سرور را فراخوانی میکند تا Representation یک Entity را روی سرور دستکاری کند. این بخش مفهوم Representational در REST را توضیح میدهد.
اما State Transfer یعنی چه؟ فرض کنید کلاینت ابتدا عملیات «ایجاد Product» را فراخوانی میکند. پس از ایجاد، حالت بعدی Product میتواند عملیاتی باشد که کلاینت قادر به فراخوانی آن است؛ برای مثال «بازیابی دادهٔ Product ایجادشده» یا «بهروزرسانی دادهٔ Product». این انتقال میان حالتهای قابل دسترس، مفهوم State Transfer را شکل میدهد.
REST فقط به این تعریف محدود نیست و بر شش Constraint استوار است:
- تفکیک مسئولیتهای کلاینت و سرور؛ کلاینت داده را نمایش میدهد و سرور آن را پردازش میکند.
- نبود Session State یا Stateless بودن؛ کلاینت و سرور برای برقراری ارتباط به نگهداری State یکدیگر وابسته نیستند.
- امکان Cache کردن Resourceها.
- ارتباط یکنواخت با Resourceهای قابل شناسایی؛ در واژگان HTTP باید URL، Response دارای Body و Header وجود داشته باشد.
- امکان افزودن Layerهای واسط، مانند Proxyها.
- امکان درخواست یک قطعه Code از سرور توسط کلاینت و اجرای آن Code در سمت کلاینت.
در این کتاب عمدتاً چهار مورد اول را در طراحی REST APIها بهکار خواهیم گرفت.
شیوههای درست REST
پیشتر وعده دادم به URI و URL بازگردیم. اکنون شیوههای مناسب تعریف آنها در REST را بررسی میکنیم.
Base URL
ابتدا Base URL را تعیین میکنیم. Base URL ریشهٔ همهٔ Endpointهای HTTP است. برای یک وبسایت معمولی استفاده از Domain دقیق کسبوکار همراه Path و Parameterهای طولانی قابل قبول است، اما برای REST APIها URLهای ساده توصیه میشوند. فرض کنید شرکت شما وبسایت فروش Product دارد و همان قابلیتها را از طریق REST API ارائه میکند. نمونهٔ زیر برای API مناسب نیست:
https://www.mycompany.com/home/services/rest
برای API بهتر است نشانی معنادارتر و سادهتری مانند این انتخاب شود:
https://api.mycompany.com
Media Type
قالب JSON همراه Header زیر توصیه میشود:
Content-Type: application/json
این گزینه رایج، عملی و کمهزینه است. XML نیز قابل استفاده است، اما JSON معمولاً به دلیل سادگی Syntax و همچنین Performance (کارایی) بهتر در Serialization/Deserialization ترجیح داده میشود. Serialization فرایندی است که یک Data Structure را به قالبی قابل ذخیره یا انتقال تبدیل میکند و Deserialization مسیر معکوس را انجام میدهد.
فرض کنید ساختار دادهٔ Product شامل شناسه، نام و توضیح باشد. Listing 1-1 نمایش JSON آن را نشان میدهد.
Listing 1-1. Serialization ساختار Product به JSON
{
"Id": 1,
"name": "My product name,"
"description": "My product description"
}
Listing 1-2 همان ساختار را در XML نشان میدهد.
Listing 1-2. Serialization ساختار Product به XML
<?xml version="1.0" encoding="utf-8"?>
<product xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Id>1</Id>
<Name>My product name</Name>
<Description>My product description</Description>
</product>
JSON در مقایسه با XML کمحجمتر و خواناتر است و Verbosity کمتری دارد.
موضوع دیگری که مستقیماً Constraint مربوط به HTTP نیست، استفاده از Content-Type مناسب برای کاهش برخی خطرها مانند Cross-Site Request Forgery (CSRF) است. در چنین حملهای Code سمت کلاینت، مانند JavaScript، ممکن است عملیات ناخواستهای را در مرورگر کاربر اجرا کند. وادار کردن تبادل داده به Serialization مشخصی مانند JSON یا XML میتواند بخشی از کنترلهای محافظتی باشد. برای مطالعهٔ بیشتر میتوان به مطلب CSRF در وبسایت OWASP مراجعه کرد.
نامگذاری URL
این موضوع را با جدیت دنبال میکنم، زیرا اگر URLها درست نوشته شوند، بدون افزودن کلمات غیرضروری میتوان از روی خود نشانی فهمید چه کاری انجام میدهند. فرض کنید میخواهید فهرست کامل Productهای Database را بازیابی کنید. نوشتن URL به شکل زیر مناسب نیست:
این URL عملاً یک جمله است. شکل سادهتر و روشنتر چنین است:
Verb برابر GET از قبل عمل را بیان میکند و نیازی نیست واژهٔ get داخل URL تکرار شود. استفاده از اسم جمع products کافی است.
اگر فقط ده Product بخواهیم، این شکل را ننویسید:
(GET) /getSomeProducts?limit=10
بلکه بنویسید:
برای ایجاد Product نیز از این استفاده کنید:
نه اینها:
POST /products/create
POST /createProduct
در منطق State Transfer، Response عملیات POST میتواند Status برابر 201 Created و ID منبع ایجادشده را در Header یا Payload برگرداند. سپس برای بازیابی Product ایجادشده از این URL استفاده میشود:
و نه:
همین منطق برای ویرایش و حذف نیز ادامه پیدا میکند. مجموعهٔ CRUD به این صورت است:
- Create:
POST /products - Retrieve:
GET /products/{id}
- Update:
PUT یا PATCH /products/{id} - Delete:
DELETE /products/{id}
این الگو برای Resourceهای مرتبط هم صدق میکند. اگر Product به Category وابسته باشد و Product متعلق به Category مشخصی باشد، برای دسترسی یا دستکاری Productهای آن Category بهتر است Path چنین باشد:
/categories/{categoryId}/products
برای Product مشخص در Category مشخص:
/categories/{categoryId}/products/{productId}
در این حالت CRUD چنین میشود:
- Create:
POST /categories/{categoryId}/products - Retrieve:
GET /categories/{categoryId}/products/{productId} - Update:
PUT یا PATCH /categories/{categoryId}/products/{productId} - Delete:
DELETE /categories/{categoryId}/products/{productId}
البته میتوان Product را بدون دسترسی به Category مدیریت کرد. اما اگر منطق کسبوکار شما لازم میداند پیش از دستکاری Product بررسی شود که به Category خاصی تعلق دارد، این ساختار Path از استفادهٔ Query Parameter مناسبتر است. بعداً دوباره به این موضوع برمیگردیم. قراردادن ID در URL Path همان چیزی است که Routing نامیده میشود و در ASP.NET Core، categoryId و productId Route Parameter هستند.
API Versioning (نسخهبندی API)
گاهی API بهسرعت از نظر قابلیتها یا قراردادهای سرویس تکامل مییابد و تغییر در Data Structureهای مبادلهشده میان کلاینت و سرور میتواند رفتار قبلی را بشکند. کلاینتها معمولاً با سرعت کمتری تغییر میکنند و API نباید برنامههای موجود را از کار بیندازد. یکی از راهحلها API Versioning است.
یک روش رایج، قرار دادن نسخه در URL است؛ یعنی برای هر نسخهٔ API نشانی جداگانهای داشته باشیم:
https://api.mycompany.com/v1/categories
https://api.mycompany.com/v2/categories
بعداً در کتاب این روش را با جزئیات بیشتری بررسی میکنیم.
روش دیگر، تعیین نسخه با Header سفارشی است. HTTP Header استاندارد خاصی برای نسخهٔ API تعریف نکرده است؛ بنابراین میتوان Header خود را ساخت:
GET https://api.mycompany.com/categories
X-API-Version: 1
من بهندرت Versioning مبتنی بر Header را استفاده میکنم، اما گزینهای معتبر است و در فصل ۴ نمونههایی از آن خواهیم دید.
روش سوم، Media Type Versioning است که از Headerهای Accept/Content-Type برای تعیین نسخه استفاده میکند. این روش بسیار کمتر دیده میشود و در این کتاب وارد جزئیات آن نمیشوم؛ مستندات Microsoft دربارهٔ API Design اطلاعات بیشتری ارائه میکند.