REST APIها یکی از رایجترین انواع APIها هستند که مورد استفاده کسبوکارها و توسعهدهندگان قرار میگیرند. به کمک این APIها کلاینتهای مختلف مانند اپلیکیشنهای مرورگر میتوانند با سرویسها ارتباط برقرار کنند. بنابراین باید REST APIها را طوری طراحی کرد تا در ادامه کار به مشکل برنخوریم. در فرایند طراحی مواردی مانند امنیت، عملکرد و راحتی استفاده برای مصرفکنندگان معیارهای اصلی هستند.
در صورتی که به فرایند طراحی API توجه کافی نداشته باشیم ممکن مشکلاتی را برای کلاینتها به وجود آوریم که باعث عدم تمایل مصرفکنندگان به استفاده از API ما شده و توسعهدهندگان بعدی را برای نگهداری و توسعه API دچار سردرگمی کنند.
در این پست ما راهکارهایی را شرح میدهیم که به طراحی یک API خوب کمک میکنند.
قبول درخواست و پاسخ با JSON
هرچند برخی از افراد (از جمله روی فیلدینگ خالق معماری RESTful) فکر میکنند که REST APIها باید هایپرتکست برگردانند، اما REST APIها باید قادر باشند درخواستها را در قالب JSON قبول کرده و پاسخ را نیز در همین قالب ارسال کنند. در حال حاضر فرمت JSON قالب استاندارد برای تبادل داده است. تقریبا تمام فناوریهای مبتنی بر شبکه می توانند از این فرمت استفاده کنند و جاوااسکریپت متدهای داخلی برای رمزنگاری و رمزگشایی دارد. فناوریهای سمت سرور نیز کتابخانههایی دارند که به آنها کمک میکند بدون نیاز به انجام کار زیاد، فرمت JSON را رمزگشایی کنند.
برای اطمینان از ارسال پاسخ در فرمت JSON ما باید پارامتر Content-Type در هدر را معادل application/json قرار دهیم. بسیاری از فریمورکهای اپلیکیشن سمت سرور هدر پاسخ را به صورت خودکار تنظیم میکنند.
همچنین ما باید مطمئن شویم که اندپوینتهای ما JSON را به عنوان پاسخ ارسال میکنند. بسیاری از فریمورکهای سمت سرور این ویژگی را به صورت خودکار دارند.
حالا یک API نمونه را با هم بررسی میکنیم که درخواست JSON را قبول میکند. در این مثال از فریمورک بکاند Express برای Node.js استفاده کردهایم:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ |
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); app.post('/', (req, res) => { res.json(req.body); }); app.listen(۳۰۰۰, () => console.log('server started')); |
فانکشن ()bodyParser.json بدنه درخواست JSON را به یک شیء جاوااسکریپت تجزیه کرده و سپس آن را به یک شیء req.body تخصیص میدهد. بدون هیچ تغییر دیگری مقدار Content-Type را معادل application/json; charset=utf-8 قرار دهید. روش ذکرشده برای اکثر فریمورکهای بکاند قابل اجرا است.
استفاده از اسامی به جای افعال
به جای افعال از اسامی نشاندهنده موجودیتی که اندپوینت آن را استفاده میکنیم. دلیل این موضوع این است که متد درخواست HTTP از قبل یک فعل دارد. اضافه کردن فعل در مسیر اندپوینت آن را بیش از حد طولانی کرده و اطلاعات جدیدی در اختیار ما نمیگذارد.
افعال انتخابی در هر مسیر اندپوینت نیز بسته به توسعهدهنده تفاوت دارد. برای مثال برخی عبارت get را استفاده میکنند و برخی دیگر retrieve را ترجیح میدهند.
با در نظر داشتن این موضوع، ما باید مسیرهایی مثل GET /articles بسازیم که نام متد گویای عملیات است و نیازی به قرار دادن فعل ندارد. در مثال زیر /articles بیانگر یک سورس REST API است. ما با کمک Express میتوانیم اندپوینتهای زیر را برای انجام تغییرات در مقالهها اضافه کنیم:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ ۱۹ ۲۰ ۲۱ ۲۲ ۲۳ ۲۴ ۲۵ ۲۶ ۲۷ ۲۸ ۲۹ ۳۰ ۳۱ ۳۲ ۳۳ |
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); app.get('/articles', (req, res) => { const articles = &#۰۹۱;]; // code to retrieve an article... res.json(articles); }); app.post('/articles', (req, res) => { // code to add a new article... res.json(req.body); }); app.put('/articles/:id', (req, res) => { const { id } = req.params; // code to update an article... res.json(req.body); }); app.delete('/articles/:id', (req, res) => { const { id } = req.params; // code to delete an article... res.json({ deleted: id }); }); app.listen(۳۰۰۰, () => console.log('server started')); |
در کد بالا ما یک سری اندپوینت برای انجام تغییرات در مقالهها تعریف کردهایم. همانطور که میبینید، نام مسیرها حاوی هیچ فعلی نیست و فقط اسم دارند.
استفاده از تودرتویی منطقی در اندپوینتها
هنگام طراحی اندپوینتها، منطقی است که اندپوینتهای حاوی اطلاعات مشترک را در یک گروه جمع کنیم. یعنی اگر یک شیء میتواند حاوی یک شیء دیگر باشد، اندپوینتها باید طوری طراحی شوند که این مسئله را نشان دهند. این روش فارغ از ساختارمند بودن پایگاه داده، یک رویه مناسب است. در واقع، برخی از متخصصین امنیت توصیه میکنند ساختار پایگاه داده را متفاوت از اندپوینتها طراحی کنید تا در صورت وقوع حمله، به حملهکنندگان اطلاعات اضافی ندهید.
برای مثال اگر ما میخواهیم بخش نظرات یک مقاله خبری را دریافت کنیم، ما باید عبارت comments/ را در انتهای مسیر articles/ قرار دهیم:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ |
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); app.get('/articles/:articleId/comments', (req, res) => { const { articleId } = req.params; const comments = &#۰۹۱;]; // code to get comments by articleId res.json(comments); }); app.listen(۳۰۰۰, () => console.log('server started')); |
در مثال بالا ما میتوانیم متد GET را در مسیر ‘articles/:articleId/comments/’ استفاده کنیم. با این کار ما نظرات پستی که توسط articleId مشخص میشود را دریافت میکنیم. در نظر داشته باشید که در این مثال هر مقاله بخش نظرات خود را دارد که دخل هر مقاله قرار گرفتهاند.
مدیریت خطاها و بازگرداندن کد خطای مناسب
برای حذف احتمال سردرگمی هنگام بروز خطا برای کاربران API، باید خطاها را به درستی مدیریت کرده و کد پاسخ HTTP بازگردانید. به این ترتیب کاربران API اطلاعات کافی برای درک خطا خواهند داشت.
ما باید خطاها را متناسب با مشکلی که اپلیکیشن با آن روبهرو است انتخاب کنیم. برای مثال اگر ما میخواهیم داده درخواست را رد کنیم، باید کد ۴۰۰ را بازگردانیم:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ ۱۹ ۲۰ ۲۱ ۲۲ ۲۳ ۲۴ ۲۵ |
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); // existing users const users = &#۰۹۱; { email: 'abc@foo.com' } ] app.use(bodyParser.json()); app.post('/users', (req, res) => { const { email } = req.body; const userExists = users.find(u => u.email === email); if (userExists) { return res.status(۴۰۰).json({ error: 'User already exists' }) } res.json(req.body); }); app.listen(۳۰۰۰, () => console.log('server started')); |
در کد بالا ما لیستی از کاربران موجود در آرایه users به همراه ایمیل آنها را داریم. سپس اگر ما سعی کنیم یک درخواست ثبت ایمیل با ایمیلی که از قبل موجود است ارسال کنیم، ما یک خطای کد ۴۰۰ با پیام ‘User already exists’ ارسال میکنیم تا کاربران متوجه شوند که این کاربر از قبل وجود دارد.
فیلتر کردن، مرتب کردن و تقسیمبندی
پایگاههای داده پشت یک REST API میتوانند به سرعت خیلی بزرگ شوند. گاهی نیز آنقدر داده وجود دارد که نمیتوان همه آنها را یکجا برگرداند چراکه این فرایند بسیار آهسته خواهد بود و یا حتی ممکن است باعث اخلال در سیستم شود.
بنابراین ما به راههایی برای فیلتر کردن آیتمها و تقسیمبندی دادهها نیاز داریم. فیلتر کردن و تقسیمبندی کردن عملکرد سیستم را از طریق کاهش استفاده از منابع بهبود میدهد. در ادامه یک مثال ساده داریم که که در آن یک API میتواند یک کوئری رشتهای با پارامترهای مختلف قبول کرده و به ما اجازه میدهد تا آیتمها را بر اساس فیلدها فیلتر کند:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ ۱۹ ۲۰ ۲۱ ۲۲ ۲۳ ۲۴ ۲۵ ۲۶ ۲۷ ۲۸ ۲۹ ۳۰ ۳۱ ۳۲ ۳۳ ۳۴ ۳۵ |
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); // employees data in a database const employees = &#۰۹۱; { firstName: 'Jane', lastName: 'Smith', age: ۲۰ }, //... { firstName: 'John', lastName: 'Smith', age: ۳۰ }, { firstName: 'Mary', lastName: 'Green', age: ۵۰ }, ] app.use(bodyParser.json()); app.get('/employees', (req, res) => { const { firstName, lastName, age } = req.query; let results = &#۰۹۱;...employees]; if (firstName) { results = results.filter(r => r.firstName === firstName); } if (lastName) { results = results.filter(r => r.lastName === lastName); } if (age) { results = results.filter(r => +r.age === +age); } res.json(results); }); app.listen(۳۰۰۰, () => console.log('server started')); |
در کد بالا، ما متغیر req.query را برای گرفتن پارامترهای پارامترهای کوئری داریم. سپس ما مقادیر هر آیتم را استخراج کرده و با اجرای filter روی هر پارامتر کوئری، آیتمهایی که میخواهیم برگردانده شوند را پیدا میکنیم. سپس ما results را به عنوان پاسخ ارسال میکنیم. برای مثال اگر ما یک درخواست GET با مسیر و کوئری زیر داشته باشیم:
۱ ۲ ۳ |
/employees?lastName=Smith&age=۳۰ |
نتیجه به این صورت خواهد بود:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ |
&#۰۹۱; { "firstName": "John", "lastName": "Smith", "age": ۳۰ } ] |
حفظ رویههای امنیتی مناسب
بیشتر ارتباطات بین کلاینت و سرور باید خصوصی باشد. ازاینرو استفاده از SSL/TLS برای امنیت ضروری است.
بارگذاری یک گواهی SSL روی یک سرور چندان سخت نیست و هزینهای ندارد (یا هزینه آن بسیار پایین است). دلیلی وجود ندارد که به جای استفاده از کانالهای امن، از کانالهای باز استفاده کنیم. در طراحی API باید در نظر داشته باشیم که کاربران در سطوح مختلف نباید به اطلاعاتی بیش از آنچه نیاز دارند، دسترسی داشته باشند.
کش کردن داده برای بهبود عملکرد
با اضافه کردن قابلیت کش کردن و امکان باز گرداندن داده از حافظه محلی به جای کوئری کردن پایگاه داده میتوانیم عملکرد سیستم را بهبود دهیم. اما کش کردن یک نقطه ضعف هم دارد؛ کاربر ممکن است داده تاریخگذشته دریافت کند. این موضوع ممکن است هنگام دیباگ کردن در فرایند تولید مشکلاتی را به وجود آورد.
راهکارهای مختلفی از جمله Redis وجود دارند که به ما اجازه میدهند نحوه کش شدن داده را به دلخواه خودمان کنترل کنیم.
نسخهبندی API
در صورت نیاز به اعمال تغییراتی که امکان ایجاد اختلال در کلاینت دارند، باید نسخههای مختلفی از API را داشته باشیم. نسخهبندی میتواند با توجه به نسخه معنایی صورت بگیرد (مانند ۲.۰.۶، که عدد ۲ نشاندهنده تغییر بزرگ و عدد ۶ نشاندهنده پچ ششم است).
به این ترتیب، ما میتوانیم اندپوینتهای قدیمی را کمکم از چرخه خارج کرده و کاربران را به سمت استفاده از API جدید سوق دهیم.