بهترین رویه های طراحی REST API – راهنمای کاربردی


در این مقاله به بررسی بهترین رویههای طراحی REST API میپردازیم تا API-هایی طراحی کنیم که برای هر کسی که آن را مورد استفاده قرار میدهد قابل درک باشد، نسبت به تغییرات آتی منعطف باشد و دارای سرعت و امنیت مطلوبی باشد تا بتواند دادهها را به صورت امن و محرمانه در اختیار کلاینتها قرار دهد.
REST API یکی از رایجترین انواع وبسرویسهایی است که امروزه در اختیار داریم. این نوع از API-ها به کلاینتهای مختلف از جمله اپلیکیشنهای مرورگر امکان میدهند که با سرور ارتباط برقرار کنند. از این رو طراحی مناسب REST API از اهمیت زیادی برخوردار است تا در ادامه راه با موانع مختلف مواجه نشویم. در این مورد باید چیزهایی مانند امنیت، کارایی و سهولت استفاده را برای مصرفکنندگان API در نظر بگیریم.
در غیر این صورت برای کلاینتهایی که از API ما استفاده میکنند، مشکلاتی ایجاد خواهیم کرد که خوشایندشان نیست و بنابراین موجب دلسردی آنها میشود. اگر از سنتهای مورد پذیرش عمومی استفاده نکنیم، موجب بروز سردرگمی در افرادی میشویم که وظیفه نگهداری API را بر عهده دارند و همچنین کلاینتها میشویم، چون API ما با آنچه که به طور کلی رواج دارد متفاوت عمل میکند.
در این مطلب به بررسی شیوه طراحی REST API برای تسهیل درک کارکرد آن برای مصرفکنندگان، انعطافپذیری برای توسعههای آتی و امنیت و سرعت کارکرد میپردازیم. از آنجا که اپلیکیشنهای تحت شبکه به روشهای مختلفی مورد آسیب قرار میگیرند، باید اطمینان پیدا کنیم آن REST API که طراحی میکنیم، با بهرهگیری از کدهای استاندارد HTTP به خوبی از پس مدیریت خطاها بر میآید و به این ترتیب امکان حل مشکلات را به مصرفکنندگان API میدهد.
پذیرش و پاسخ با JSON
REST API-ها برای payload درخواست باید JSON بپذیرند و پاسخها را نیز در همین فرمت ارسال کنند. JSON یک استاندارد عمومی برای ارسال و دریافت دادهها محسوب میشود. تقریباً همه فناوریهای تحت شبکه میتوانند از JSON استفاده کنند. جاوا اسکریپت متدهای داخلی برای رمزگذاری و رمزگشایی JSON چه از طریق Fetch API و چه کلاینتهای دیگر HTTP دارد. فناوریهای سمت سرور نیز کتابخانههایی دارند که JSON را بدون نیاز به زحمت اضافی دیکد میکنند.
البته روشهای مختلفی برای مبادله دادهها وجود دارد. برای نمونه میتوان از XML استفاده کرد. اما بسیاری از فریمورکها از XML پشتیبانی نمیکنند و از این رو برای رمزنگاری و رمزگشایی این فرمت داده نیاز به برخی کارهای دستی خواهیم داشت. امکان دسترسی دادههای XML در سمت کلاینت و به خصوص در مرورگرها به آسانی فراهم نیامده است. بنابراین در صورت استفاده از XML به مقدار زیادی کار بیشتر برای یک ارسال ساده دادهها نیاز خواهیم داشت.
دادههای Form برای ارسال دادهها به خصوص در زمانی که قصد ارسال فایل دارید، مناسب هستند. اما در مورد متن و عدد نیازی به دادههای Form نداریم، زیرا در اغلب فریمورکها دادههای JSON به صورت مستقیم از سمت کلاینت ارسال میشوند. بنابراین JSON با اختلاف زیادی سرراستترین روش برای مبادله دادهها محسوب میشود.
برای این که مطمئن شویم وقتی اپلیکیشنهای REST API در فرمت JSON پاسخ میدهند، کلاینت میتواند آن را به درستی تفسیر کند، باید مقدار Content-Type را در هدر پاسخ به صورت application/json تنظیم کنیم. بسیاری از فریمورکهای اپلیکیشن سمت سرور، هدر پاسخ را به طور خودکار تنظیم میکنند. برخی کلاینتهای HTTP در هدر پاسخ به دنبال مقدار میگردند تا دادهها را بر آن اساس تفسیر کنند.
تنها استثنا در این زمینه زمانی است که میخواهیم فایلها را بین کلاینت و سرور ارسال و دریافت کنیم. در این حالت باید پاسخهای فایل را مدیریت کرده و دادههای فرم را از کلاینت به سرور ارسال نماییم. اما این موضوعِ مقاله جداگانهای است. همچنین باید مطمئن شویم که «نقاط انتهایی» (endpoints) نیز در پاسخ JSON بازگشت میدهند. بسیاری از فریمورکهای سمت سرور این قابلیت را به صورت داخلی دارند.
در ادامه یک نمونه API را بررسی میکنیم که payload با فرمت JSON قبول میکند. در این مثال از فریمورک بکاند Express برای Node.js استفاده میکنیم. ما میتوانیم از میانافزار body-parser (+) برای تفسیر بدنه درخواست JSON بهره بگیریم و در ادامه متد res.json را با شیئی فراخوانی کنیم که میخواهیم به صورت پاسخ JSON بازگشت یابد:
متد ()bodyParser.json رشته بدنه درخواست JSON را به یک شیء جاوا اسکریپت تفسیر میکند و سپس آن را به شیء req.body انتساب میدهد. هدر Content-Type را بدون هیچ تغییری در پاسخ روی application/json; charset=utf-8 تنظیم کنید. متد فوق روی اغلب فریمورکهای بکاند کار میکند.
استفاده از اسم به جای فعل در مسیرهای انتهایی
ما در زمان طراحی REST API نباید در نقاط انتهایی از افعال استفاده کنیم. به جای آن باید از اسمی بهره بگیریم که نماینده موجودیتی است که نقطه انتهایی ما قصد بازیابی یا دستکاری آن را دارند. دلیل این امر آن است که متد درخواست HTTP ما خود یک فعل دارد. استفاده از فعل در مسیرهای نقطه انتهای API مفید نیست و موجب طولانی شدن غیر ضروری آن میشود، زیرا هیچ نوع اطلاعات جدیدی را به دست نمیدهد.
از سویی افعال انتخابشده، بسته به میل توسعهدهنده متفاوت خواهند بود. برای نمونه برخی دوست دارند از get استفاده کنند، در حالی که برخی دیگر از retrive بهره میگیرند. رایجترین متدها شامل GET، POST، PUT و DELETE هستند. دستور GET منابع را بازیابی میکند. POST دادههای جدیدی در اختیار سرور قرار میدهد. PUT داددههای موجود را بهروزرسانی میکند. DELETE دادهها را حذف میکند. این افعال را میتوان به انواع مختلف عملیات CRUD نگاشت کرد.
با توجه به دو اصلی که مورد اشاره قرار دادیم، باید در زمان طراحی REST API از مسیرهایی مانند GET /articles/ برای دریافت مقالات جدید استفاده کنیم. به طور مشابه، POST /articles/ برای ارسال مقالات جدید استفاده میشود. PUT /articles/:id برای بهروزرسانی یک مقاله موجود با id مفروض مورد استفاده قرار میگیرد. همچنین DELETE /articles/:id موجب حذف شدن مقاله با id مفروض کاربرد خواهد داشت.
از سوی دیگر مسیر /articles منبع REST API ما را عرضه میکند. برای نمونه میتوانیم از Express برای افزودن نقاط انتهایی زیر جهت دستکاری مقالات بهره بگیریم:
در کد فوق نقاط انتهایی لازم برای دستکاری مقالات را تعریف کردهایم. چنان که میبینید، نامهای مسیر شامل هیچ فعلی نیستند و تنها از اسم استفاده شده است. فعلهای مورد نیاز به صورت افعال HTTP مورد استفاده قرار گرفتهاند.
نقاط انتهایی POST، PUT و DELETE همگی JSON را به عنوان بدنه درخواست میپذیرند و همه آنها نیز در پاسخ JSON بازگشت میدهند که شامل نقطه انتهایی GET نیز میشود.
نامگذاری کلکسیونها با اسمهای جمع
ما باید از اسامی جمع برای کلکسیونها استفاده کنیم. مواردی که بخواهیم تنها یک آیتم منفرد را از بکاند دریافت کنیم، بسیار نادر هستند و از این رو باید در نامگذاری هم این نکته را رعایت کرده و از اسامی جمع بهره بگیریم.
ما از اسامی جمع برای مطابقت با محتوای پایگاههای داده استفاده میکنیم. جدولها معمولاً بیش از یک مدخل دارند و از این رو طوری نامگذاری میشوند که با این موضوع مطابقت داشته باشند. بنابراین برای دسترسی به API نیز از همین منطق جداول استفاده کنیم. زمانی که از نقطه انتهایی articles/ استفاده میکنیم، شکل جمع را برای همه نقاط انتهایی داریم و از این رو دیگر لازم نیست چیزی را به حالت جمع تغییر دهیم.
منابع تودرتو برای اشیای سلسله مراتبی
مسیر نقطه انتهایی که برای کار با منابع تودرتو استفاده میشود باید از طریق الحاق منابع تودرتو به همان شیوه نام مسیری که پس از منبع والد میآید ساخته شود.
در این زمینه باید مطمئن شویم که یک منبع تودرتو با محتوای جداول تطبیق دارد. در غیر این صورت موجب بروز سردرگمی خواهد شد. برای نمونه اگر بخواهیم یک نقطه انتهایی، کامنتها را برای یک مقاله خبری به دست آورد، باید مسیر comments/ را به انتهای مسیر articles/ اضافه کنیم. در این حالت فرض میکنیم که comments به عنوان یک فرزند article در پایگاه داده ما ذخیره شدهاند.
به عنوان مثال، میتوانیم به صورت زیر در اکسپرس کدنویسی کنیم:
در کد فوق، میتوانیم از متد GET روی مسیر '/articles/:articleId/comments' استفاده کنیم. به این ترتیب comments را روی مقاله مشخص شده با articleId به دست میآوریم و سپس آن را در پاسخ بازگشت میدهیم. ما 'comments' را پس از قطعه مسیر '/articles/:articleId' اضافه میکنیم تا مشخص شود که یک منبع فرزند articles/ است.
این کار درستی است زیرا کامنتها (comments) اشیای فرزند مقالات (articles) هستند و هر مقاله، کامنتهای خاص خود را دارد. در غیر این صورت اوضاع برای کاربر پیچیده میشود، زیرا این ساختار به طور کلی برای دسترسی به اشیای فرزند پذیرفته شده است. همین اصل در مورد نقاط انتهایی POST، PUT و DELETE نیز صدق میکند. همگی این نقاط انتهایی میتوانند از همان نوع ساختار تودرتو برای نام مسیرهای مختلف استفاده کنند.
مدیریت صحیح خطاها و بازگشت دادن کدهای استاندارد خطا
برای این که در زمان بروز خطا سردرگمی کاربران API رفع شود، باید مدیریت صحیحی روی خطاها اعمال کرده و کدهای پاسخ HTTP را طوری بازگشت دهیم که نوع خطای رخ داده را نمایان سازند. به این ترتیب افرادی که API را نگهداری میکنند اطلاعات کافی برای درک مشکل رخداده در دست خواهند داشت. ما نمیخواهیم خطاها موجب از کار افتادن سیستم شوند و از این رو نباید آنها را مدیریت نشده رها کنیم و مدیریت خطاها را به عهده مصرفکننده API بسپاریم.
کدهای رایج وضعیت HTTP به شرح زیر هستند:
- 400 Bad Request – این خطا به معنی ناموفق بودن اعتبارسنجی ورودی سمت کلاینت است.
- 401 Unauthorized – به این معنا است که کاربر اجازه دسترسی به منبع را ندارد. این خطا عموماً زمانی بازگشت مییابد که کاربر احراز هویت نشده باشد.
- 403 Forbidden – این خطا به معنای آن است که کاربر احراز هویت شده، اما اجازه دسترسی به منبع را ندارد.
- 404 Not Found – این خطا نشان میدهد که منبع مورد تقاضا موجود نیست.
- 500 Internal server error – این یک خطای عمومی سرور است. این خطا نباید به صورت صریح صادر شود.
- 502 Bad Gateway – این خطا نشان میدهد که پاسخ غیر معتبری از سرور بالادستی دریافت شده است.
- 503 Service Unavailable – این خطا نشان میدهد که مشکلی در سمت سرور رخ داده است. این خطا میتواند هر چیزی از قبیل اضافه بار سرور، عدم امکان دسترسی به برخی مسیرهای سیستمی و غیره باشد.
ما باید خطاهایی صادر کنیم که متناسب با خطای رخ داده باشند. برای نمونه اگر میخواهیم دادهها را از payload درخواست حذف کنیم، در این صورت باید پاسخ 400 را به صورت زیر در API اکسپرس ارسال نماییم:
در کد فوق فهرستی از کاربران موجود در آرایه users با ایمیل مفروض در اختیار داریم.
در ادامه اگر بخواهیم payload را با مقدار email که در users وجود دارد تحویل دهیم، یک کد وضعیت پاسخ 400 با پیام 'User already exists' ارائه میکنیم تا کاربر بداند که کاربر مورد نظر از قبل وجود دارد. با بهرهگیری از این اطلاعات، کاربر میتواند با تغییر دادن ایمیل به چیزی که وجود ندارد، کار مورد نظر خود را انجام دهد.
کدهای خطا باید همراه با پیامهایی ارائه شوند تا نگهداریکنندگان API اطلاعات کافی برای عیبیابی خطا در دست داشته باشند، اما مهاجمان نتوانند از محتوای خطا برای اجرای حملههایی مانند سرقت اطلاعات یا از کار انداختن سیستم کمک بگیرند. بنابراین هر زمان که API با موفقیت اجرا نشد، باید با ارسال یک خطا به همراه اطلاعات مقتضی به کاربران کمک کنیم تا اقدام اصلاحی را اجرا نمایند.
فراهم ساختن امکان فیلتر کردن، مرتبسازی و صفحهبندی
پایگاههای دادهای که پشت یک REST API قرار دارند، میتوانند بسیار بزرگ باشند. گاهی اوقات، حجم دادهها آن قدر بالا است که امکان بازگشت دادن همه آنها در یک وهله وجود ندارد چون به دلیل حجم بالا یا بسیار کند خواهد بود و یا موجب از کار افتادن سیستم میشود. از این رو باید روشی برای فیلتر کردن آیتمها داشته باشیم.
همچنین باید برخی روشها برای صفحهبندی دادهها داشته باشیم تا در هر بار تنها بخشی از دادهها را بازگشت دهیم. فیلتر کردن و صفحهبندی دادهها هر دو موجب افزایش کارایی سیستم از طریق استفاده از منابع سرور میشود. زمانی که دادههای زیادی در پایگاه داده انباشت میشوند، داشتن این قابلیتها اهمیت به مراتب بالاتری مییابد.
در ادامه مثالی از یک API را میبینید که میتواند یک رشته کوئری با پارامترهای کوئری مختلف بپذیرد تا آیتمها را بر مبنای فیلدهایشان فیلتر کند:
در کد فوق یک متغیر به نام req.query داریم که پارامترهای کوئری را دریافت میکند. سپس مقادیر مشخصه را به وسیله ساختار destructuring جاوا اسکریپت با تجزیه پارامترهای منفرد کوئری در متغیرهای مختلف ذخیره میکنیم. در نهایت filter را روی هر مقدار پارامتر کوئری اجرا میکنیم تا آیتمهایی که قرار است بازگشت یابند را پیدا کنیم.
زمانی که همه این کارها انجام شد، results را به عنوان پاسخ بازگشت میدهیم. بدین ترتیب هنگامی که یک درخواست GET به مسیر زیر با استفاده از رشته کوئری ارسال میشود:
/employees?lastName=Smith&age=30
نتیجه زیر به دست میآید:
چنان که در پاسخ بازگشتی میبینید، نتیجه بر اساس lastName و age فیلتر شده است.
به طور مشابه، میتوانیم پارامتر کوئری page را قبول کرده و یک گروه از موجودیتها در موقعیت مورد نظر از (page - 1) * 20 تا page * 20 بازگشت دهیم. همچنین میتوانیم درخواست مرتبسازی فیلدها را در رشته کوئری ارسال کنیم. برای نمونه میتوانیم پارامتر را از یک رشته کوئری با فیلدهایی که قرار است دادهها بر اساس آن مرتبسازی شوند دریافت کرده و در ادامه آنها را بر اساس فیلدهای منفرد مرتبسازی میکنیم. برای نمونه فرض کنید میخواهیم رشته کوئری را از یک URL مانند زیر استخراج کنیم:
http://example.com/articles?sort=+author,-datepublished
در رشته فوق علامت بعلاوه (+) به معنای صعودی و علامت منها (-) به معنای ترتیب نزولی است. بنابراین دادهها را بر اساس نام مؤلف به ترتیب الفبایی مرتبسازی میکنیم و datepublished را نیز از جدیدترین به قدیمیترین مواد مرتب خواهیم کرد.
حفظ رویههای امنیتی مناسب
اغلب ارتباط بین کلاینت و سرور باید خصوصی باشد، زیرا در اغلب موارد اطلاعاتی خصوصی ارسال و دریافت میشوند. از این رو استفاده از SSL/TLS برای حفظ امنیت یک ضرورت محسوب میشود. بارگذاری یک گواهینامه SSL روی سرور کار چندان دشواری نیست و هزینه آن رایگان یا بسیار پایین است. از این رو دلیلی برای عدم مبادله دادههای REST API روی کانالهای امن و استفاده از کانالهای باز وجود ندارد.
افراد نباید به دادههایی بیش از آنچه تقاضا کردهاند دسترسی یابند. برای نمونه یک کاربرد عادی نباید به اطلاعات کاربر دیگر دسترسی داشته باشد. همچنین نباید به دادههای مدیریتی دسترسی پیدا کند. برای الزام اصل حفظ کمینه دسترسیها باید بررسیهای نقش را به یک نقش منفرد اضافه کرده و یا نقشهای انفرادی برای هر کاربر داشته باشیم.
اگر بخواهیم کاربران را در چند گروه دستهبندی کنیم، در این صورت نقشها باید مجوزهایی داشته باشند که هر آنچه لازم است و نه بیشتر را پوشش دهند. اگر مجوزهای منفرد بیشتری برای قابلیتهای خاص داشته باشیم که کاربران به آنها دسترسی دارند، باید مطمئن شویم که ادمینها میتوانند این قابلیتها را از کاربران حذف و یا به آنها اضافه کنند. همچنین باید برخی نقشهای از پیش تنظیم شده داشته باشیم که بتوانند روی کاربران گروهی اعمال شوند به طوری که نیاز به انجام این کار به طور دستی برای هر کاربر وجود نداشته باشد.
کش کردن دادهها جهت بهبود کارایی
امکان اضافه کردن لایه کش برای بازگرداندن نتایج بازگشتی از کش حافظه لوکال به جای کوئری زدن به پایگاه داده و دریافت دادهها در موارد متعدد که کاربران دادههای یکسانی درخواست میکنند نیز وجود دارد. نکته خوب در مورد کش کردن این است که کاربران میتوانند دادهها را سریعتر دریافت کنند. با این حال، ممکن است دادههایی که کاربران دریافت میکنند، قدیمی باشند. همچنین این وضعیت ممکن است منجر به مشکلاتی در زمان دیباگ کردن در محیط پروداکشن شود، زیرا خطایی رخ داده است، اما ما همچنان دادههای قدیمی را مشاهده میکنیم.
راهحلهای کش کردن به شیوههای مختلفی ارائه شدهاند که شامل استفاده از Redis، کش کردن درون حافظه و موارد دیگر میشود. بنابراین ما میتوانیم شیوه کش شدن دادهها را بر اساس نیازهای خود تغییر دهیم.
برای نمونه Express یک میانافزار به نام apicache دارد که امکان کش کردن را بدون نیاز به پیکربندی زیاد به اپلیکیشن اضافه میکند. به این ترتیب میتوانیم یک کش ساده درون حافظهای را به صورت زیر به سرور خود اضافه کنیم:
کد فوق صرفاً یک ارجاع به میانافزار apicache با استفاده از apicache.middleware ارائه میکند و در ادامه کدی مانند زیر داریم:
app.use(cache('5 minutes'))
که کل اپلیکیشن را کش میکند. به این ترتیب ما نتایج را به مدت پنج دقیقه کش میکنیم. با این حال امکان تنظیم این مقدار بسته به نیاز وجود دارد.
نسخهبندی API
اگر تغییراتی در API داده میشود که ممکن است موجب از کار افتادن کلاینتها شود، باید حتماً نسخههای مختلفی برای API داشته باشیم. نسخهبندی میتواند مانند کاری که اغلب اپلیکیشنها امروزه انجام میدهند، بر اساس روش نسخههای معنایی انجام گیرد. برای نمونه 2.0.6 نشانگر نسخه اصلی 2 و پچ ششم است.
به این ترتیب میتوانیم به جای این که کاربران را ملزم کنیم که همگی به یکباره به نسخه جدید API مهاجرت کنند، به تدریج نقاط انتهایی قدیمی را از دور خارج کنیم. در این حالت نقطه انتهایی v1 میتواند برای افرادی که تمایلی به تغییر ندارند، همچنان فعال باقی بماند، در حالی که v2 به همراه قابلیتهای جدیدش در اختیار کسانی است که قصد ارتقا دارند. این وضعیت به طور خاص در مواردی مهم است که API ما عمومی باشد. ما باید API-ها را طوری نسخهبندی کنیم که منجر به از کار افتادن اپلیکیشنهای شخص ثالث که از آنها استفاده میکنند نشود.
نسخهبندی به طور معمول با اضافه کردن /v1/ یا /v2/ و غیره به ابتدای مسیر API انجام مییابد. برای نمونه میتوانیم در اکسپرس به طور زیر عمل کنیم:
ما صرفاً شماره نسخه را به ابتدای نقاط انتهایی مسیر URL اضافه کردهایم تا آنها را نسخهبندی کنیم.
سخن پایانی: طراحی REST API
در این مطلب با بهترین رویههای طراحی REST API آشنا شدیم. برای جمعبندی مهمترین نکات این مقاله باید اشاره کنیم که طراحی باید استانداردها و سنتهای وب را رعایت کند. استفاده از فرمت JSON برای دادهها، بهرهگیری از پروتکل SSL/TLS و کدهای وضعیت HTTP همگی اجزایی هستند که وب مدرن را تشکیل دادهاند. کارایی و عملکرد API نیز یک ملاحظه جدی است. برای افزایش کارایی API باید از بازگرداندن دادههای زیاد در هر وهله از کوئری اجتناب کنیم. امکان استفاده از کش برای جلوگیری از اجرای مکرر کوئریهای تکراری روی پایگاه داده نیز وجود دارد.
مسیرهای نقاط انتهایی باید منسجم باشند. ما از اسمها تنها از این رو استفاده میکنیم که متدهای HTTP نشان میدهند که میخواهیم چه کاری انجام بدهیم. مسیرهای منابع تودرتو باید پس از منبع والد بیاید. آنها باید بدون نیاز به خواندن مستندات بیشتر آنچه که به دست میآوریم یا دستکاری میکنیم را برای ما مشخص سازند.