از زمانی که قابلیتهای AJAX در مرورگرهای ما بیش از ۲۰ سال پیش آمد، توسعهدهندگان در سرتاسر جهان مشغول استفاده از این فناوریها به محدودیتهای خود، مهندسی روشهای خلاقانه برای به چالش کشیدن پایههای اینترنت اصلی، و تبدیل آن از یک وب از صفحات ثابت به شبکه ای از داده های پویا جریان می یابد.
رابطهای برنامهنویسی کاربردی (API) که همه این تبادل دادهها را تامین میکنند، تکرارهای زیادی را پشت سر گذاشتهاند، و جای تعجب نیست که در سال 2023 بسیاری از تیمهای توسعه با تصمیمگیری در مورد طراحی و پیادهسازی API خود دست و پنجه نرم میکنند. با انواع زبانهای برنامهنویسی، پروتکلهای انتقال و معماریهای زیرساخت، یافتن راهی برای تعریف قراردادهای قابل اعتماد برای انتقال دادهها، و بیشتر از آن برای پیادهسازی و حفظ آنها در مواجهه با نیازهای متغیر، کار سادهای نیست.
با سر و کار داشتن روزانه با APIها، و مشاهده بحث های پیرامون طراحی آنها در محافل حرفه ای ام، به نظرم مفید بود که برخی از افکارم را در این مقاله بیان کنم. با توجه به اینکه REST، GraphQL و RPC متداولترین الگوهای طراحی هستند، من روی آنها تمرکز میکنم و برخی از نظرات و دیدگاه و تجربیات خود را خلاصه میکنم. پس از کار با هر 3 مورد، متوجه شدم که هیچ چیز خاصی نیست، و فکر می کنم ما با داشتن یک زمینه مشترک، مشابه استانداردهای W3C، که به ایجاد حس و ساختار به فناوری های مرورگر کمک کرد، فاصله زیادی داریم.
قبل از شروع، اجازه دهید برخی از موارد پایه را روشن کنم:
به نوعی، برابری با این 3 پارادایم مانند مقایسه سیب با پرتقال است. از این گذشته، GraphQL فقط یک مشخصات زبان کوئری است و می تواند به روشی بسیار شبیه به دروازه RPC باشد. در سمت کوئری، همچنین با تمرکز بر منابع بسیار شبیه به REST است، و در واقع هیچ چیز مانع از ارسال اسناد GraphQL به عنوان بخشی از کال های REST شما نمی شود و از حل کننده های فیلد به عنوان بخشی از اجرای کنترلر خود استفاده می کنید. بنابراین، به خاطر این مقاله، GraphQL را به عنوان پیادهسازی مشخصات، یعنی سروری مشابه Apollo که قادر به دریافت و سرویس دادهها از طریق HTTP است، در نظر خواهم گرفت.
صحبت از RPC، من به هیچ پیادهسازی خاصی مانند gRPC اشاره نمیکنم، بلکه به پارادایم استفاده از فراخوانی روند به جای کار بر روی منابع با آدرسهای منحصربهفرد در شبکه، همانطور که در REST معمول است، اشاره میکنم. حتی اگر قصد دارم با پروتکلهای انتقال دیگر تشابهاتی داشته باشم، برای اهداف این مقاله، API به معنای انتقال داده از طریق HTTP است. من ملاحظات شبکه را کنار می گذارم تا بر روی طراحی API واقعی تمرکز کنم تا مکانیک انتقال داده ها از طریق سیم. به این ترتیب، بهتر است به اجرای RPC مشابه JSON-RPC یا tRPC به جای gRPC (با استفاده از HTTP/2 برای جریان دوطرفه داده های باینری) فکر کنید.
بیایید به جنبههای مختلف طراحی API که باید در هنگام تصمیمگیری در نظر گرفته شوند نگاهی بیاندازیم و ببینیم که چگونه این 3 الگو در برابر این الزامات مقاومت میکنند.
اسکیما چیست؟
توانایی بیان APIها با عبارات انتزاعی یا همان آبسترک با استفاده از یک زبان تعریف اسکیما (SDL) نکته مهمی است که هنگام انتخاب طرح API ما باید مورد توجه قرار گیرد. SDL ها به ما امکان می دهند طراحی خود را از جزئیات پیاده سازی جدا کنیم، روی مدل داده خود تمرکز کنیم و یک قرارداد را به گونه ای بیان کنیم که با انواع کلاینت هایی که می توانند در آینده با API ما تعامل داشته باشند، قابل همکاری باشد. SDL ها ساخت ابزار برای خودکار کردن برخی از فرآیندهای دستی پر زحمت را آسان تر می کنند، به عنوان مثال. با ایجاد SDK یا انواع کلاینت برای اشیاء انتقال داده ما.
کار بدون SDL یک مشکل است، زیرا در نهایت مجبور می شوید مجموعه ای از اسناد را در قالب های تصادفی نگهداری کنید که به روز نگه داشتن، تکامل و برقراری ارتباط با کلاینت API دشوار خواهد بود.
هنگام ارزیابی یک SDL در نظر بگیرید که گردش کار توسعه شما چگونه خواهد بود:
رویکرد اسکیما اول برای تیم های بزرگتر که با پیچیدگی سازمانی سروکار دارند، عملی است. تیمهای مختلف میتوانند قبل از پیشبرد اجرای واقعی، روی تعریف قرارداد با یکدیگر همکاری کنند. این رویکرد به زمان بیشتری در برنامه ریزی و ارتباط نیاز دارد، اما در نتیجه به تیم ها اجازه می دهد تا به طور مستقل و موازی کار کنند، بدون اینکه با برنامه های اجرایی متفاوت سروکار داشته باشند. این همچنین منجر به کیفیت بهتر کد می شود زیرا چنین جریان توسعه ای مستلزم ساختن ساختگی ها و آزمایشات است، شاید حتی توسعه را به سمت TDD سوق دهد. مزیت اضافی رویکرد طرح اول این است که نسبت به جزئیات پیادهسازی واقعی بیاعتنا است،فناوری زیربنایی را همیشه میتوان بدون تأثیر بر گردش کار یا تأثیرگذاری بر کلاینت، تعویض کرد. با این حال، استفاده از این رویکرد مستلزم آن است که همه یاد بگیرند به یک زبان جدید صحبت کنند، بنابراین سینتکس و قابلیت استفاده ملاحظات بسیار مهمی هستند.
رویکرد کد اول برای تیمهای کوچکتری که میخواهند چابکی را حفظ کنند، در عین حال که سطحی از ایمنی قراردادی بین کلاینت ها دارند و از ابزارسازی در اکوسیستم SDL بهره میبرند، عملیتر است. رویکرد کد اول به شدت با ابزار انتخاب شده برای توسعه همراه است و خطر تلاش پرهزینهای را به همراه دارد که برای تغییر به رویکرد اول اسکیما در آینده لازم است، به عنوان مثال. زمانی که ابزارها دیگر توسط نویسندگان خود نگهداری نمی شوند، یا پیچیدگی سازمانی فزاینده مانع از همکاری موثر تیم می شود.
GraphQL در اصل یک مشخصات SDL است که آن را به یک نامزد خوب برای بیان API تبدیل می کند. دارای پشتیبانی داخلی برای تعریف رویه ها (کوئریها و جهش ها)، انواع ورودی و پاسخ است.
REST و RPC مفاهیم آبسترک هستند و SDL استاندارد ندارند، اما چندین چارچوب با هدف کلی وجود دارد که میتوان برای توصیف APIهای مبتنی بر HTTP از آنها استفاده کرد.
OpenAPI (Swagger سابق) رایج ترین مشخصات SDL است که به نویسندگان API اجازه می دهد درخواست ها و پاسخ های HTTP را در YAML یا JSON بیان کنند. اکوسیستم OpenAPI دارای تعدادی ابزار توسعه جالب است.
AsyncAPI گزینه دیگری است که باید برای RPC و سایر سیستم هایی که به جای منابع بر تبادل پیام ها تمرکز دارند در نظر گرفت. سینتکس آن شبیه OpenAPI است، اما می تواند نیازهایی را که فراتر از پروتکل HTTP هستند را بهتر بیان کند.
APIهایی که از طریق SDL بیان میشوند همچنین به ما کمک میکنند تا نگرانیهای مربوط به ایمنی نوع و سریالسازی را برطرف کنیم و قراردادها را در سراسر استک های ما قابل اجرا میسازند. چندین الگوی سریالسازی و اعتبارسنجی وجود دارد که میتوانیم آنها را بر روی مشخصات خود بسازیم.
Protocol Buffers یک پلت فرم و مکانیسم خنثی زبان برای سریال سازی داده های ساخت یافته است. مدلهای بیان شده در OpenAPI را میتوان به فرمت protobuf با gnostic صادر کرد که تبادل مطمئن دادهها بین سرور و کلاینتها را آسانتر میکند.
JSON-Schema استاندارد دیگری برای بیان داده های قابل سریال سازی و همچنین الزامات اعتبارسنجی برای چنین داده هایی است. مشخصات OpenAPI را می توان با استفاده از مبدل ها به JSON-Schema تبدیل کرد. اگر می خواهید داده های خود را در مراحل مختلف چرخه عمر درخواست تأیید کنید، رویکرد جالبی است.
Modelina و سایر ابزارها می توانند برای تولید مدل از OpenAPI، AsyncAPI و JSON-Schema استفاده شوند.
هنگام تعریف اسکیما خود، ممکن است بخواهید قراردادهای موجود را نیز بررسی کنید که اطمینان حاصل می کند که طراحی API شما تا حد امکان با سیستم های دیگری که ممکن است بخواهید در آینده با آنها یکپارچه شوید سازگار است.
JSON-RPC یک قرارداد برای ساختاربندی پیام های RPC شما است.
Cloudevents تلاش گستردهتری برای یافتن زبان مشترک هنگام مستندسازی رویدادها در قالبهای مختلف است و میتواند برای ملاحظات مختلف RPC مفید باشد.
قراردادهای فیلترینگ، مرتبسازی و صفحهبندی مختلفی را که قبلاً وجود داشتهاند، در نظر بگیرید، به عنوان مثال. SCIM، OData، CQL.
موارد دیگری که باید هنگام برخورد با SDL در نظر داشته باشید:
آیا این امکان را به شما می دهد که منابع خود را به گونه ای توصیف کنید که به هر کلاینت اجازه دهد منطق، منابع و روابط را به راحتی درک کند؟
آیا این امکان را به شما می دهد تا الزامات احراز هویت و دسترسی به منابع را بر اساس نقش/مجوزهای کلاینت بیان کنید؟
آیا از تمام انواع داده ها (اسکالرها، enums و اتحادیه های پیچیده) که برنامه شما نیاز دارد پشتیبانی می کند؟
آیا ابزار لازم برای سریالسازی دادهها در قالبی مناسب برای HTTP (مانند جستجوی URL و JSON) و سایر پروتکلها را دارد؟
آیا به شما امکان می دهد مدل داده، منابع و رویه های خود را فراتر از پروتکل HTTP بیان کنید، یعنی ترجیح می دهید از همان SDL برای توصیف انتقال داده در سایر پروتکل های ارتباطی (مانند صف های پیام یا حتی رابط های خط فرمان) استفاده کنید.
نکته مهم در مورد SDL این است که می تواند به عنوان سند برای API شما، هم در داخل و هم در خارج عمل کند. در دسترس قرار دادن اسکیما برای کلاینت ها این امکان را برای آنها فراهم می کند تا اسکیما را برای کشف منابع موجود در API خود بررسی کنند. زمین های بازی GUI فوق العاده ای وجود دارد که به شما امکان می دهد اسکیما های GraphQL (به عنوان مثال Apollo Studio) و OpenAPI (به عنوان مثال Swagger UI) را کاوش کرده و با آنها تعامل داشته باشید. و البته آنها به خوبی با پستچی محبوب ما ادغام می شوند و به ما امکان می دهند مجموعه هایی را از تعاریف اسکیما ایجاد کنیم و حتی مجموعه ها را به برخی از SDL های رایج صادر کنیم.
مدل داده
تصمیم در مورد طراحی API نمی تواند بدون درک خوب از ویژگی ها و پیچیدگی مدل داده های اساسی و نیازهای کلاینت در دسترسی به منابع اتخاذ شود.
پیچیدگی شی - رابطه ای
پیچیدگی مدل رابطهای یک نگرانی عمده در طراحی API است، زیرا عبور از مدلهای رابطهای پیچیده از طریق API میتواند دشوار باشد. روابطی که بین موجودیت های دامنه شما وجود دارد می تواند یک عامل تعیین کننده به نفع یا علیه یک رویکرد خاص باشد.
در اجرای استاندارد REST، هر منبع آدرس شبکه منحصر به فرد خود را دارد. شما می توانید با یک درخواست ساده GET به منبعی دسترسی پیدا کنید، به عنوان مثال. GET /users/1. وقتی سعی میکنید مجموعهها و روابط را نشان دهید، اوضاع تیرهتر میشود، به عنوان مثال. برای دریافت لیستی از دوستان کاربر باید به GET /users/1/friends بروید، اما اگر بخواهید دوست خاصی داشته باشید مبهم می شود: کاملاً مشخص نیست که آیا از طریق GET /users/1/friends/friendship به آن دسترسی دارید یا خیر. -1 یا GET /friendships/friendship-1 یا GET /users/2. همچنین مشخص نیست که اسناد تودرتو را در کجا قرار دهید، به عنوان مثال. اگر یک آدرس کاربر به عنوان بخشی از منبع کاربر برگردانده شود، یا باید منبع خود آن باشد که در لبه GET /user/1/addresses زندگی می کند. چنین ابهامی تیم های توسعه را با تصمیمات بی پایان در مورد مرزهای دامنه و نمایش آنها در نمودار REST مواجه می کند. مصالحه هایی بین یکنواختی مدل دامنه و عملکرد ایجاد می شود و این امر باعث می شود که کلاینت ها API بدانند دقیقا به کجا نگاه کنند.
جدای از آن، REST بار عبور از نمودار را بر روی کلاینت API قرار می دهد. اگر تا به حال سعی کرده اید لیستی از روابط درجه دوم را حل کنید، می دانید که چه کابوس است، با مجموعه ای از صدها، اگر نه هزاران درخواست API. علاوه بر این، اگر با روابط دوطرفه سر و کار دارید، باید تکرار گره و همچنین دایرهای بودن را در نظر بگیرید، و این مسئولیت را بر عهده کلاینت میگذارد که نوعی از حافظه پنهان و حذفسازی را برای جلوگیری از حلقههای بینهایت پیادهسازی کند.
برای بهبود این مشکل، نویسندگان REST API به راهحلهای خلاقانه (معروف به هک) متوسل میشوند که به هنگام درخواست منبع اجازه میدهد روابط حل شود. به عنوان مثال، Stripe API از پارامتر گسترش استفاده می کند. این نگرانیهای پیمایش را برطرف میکند، اما منجر به انواع پاسخهای غیر قطعی میشود، که مجدداً باید توسط کلاینت API رسیدگی شود و آن را مجبور به انجام بسیاری از شعبدهبازیهای نوع و اجبار میکند.
با ظهور شبکه های اجتماعی و نمودارهای اجتماعی پیچیده آنها، REST کاملا غیر قابل مدیریت شده بود. GraphQL، همانطور که از نام آن پیداست، توسط فیس بوک به عنوان راهی برای عبور از یک نمودار اجتماعی پیچیده ایجاد شد که نشان دهنده یک کل جامعه انسانی با شش درجه جدایی آن است. فیس بوک در تلاش بود تا با تعداد زیادی بازیگر و روابط چند جهته بین آنها، مشکلی را که مربوط به پیچیدگی اجتماعی است، حل کند، و شما باید هنگام ارزیابی اینکه آیا طراحی API شما ممکن است از آن بهره مند شود یا خیر، این مسئله را در نظر داشته باشید.
GraphQL در سادگی خود درخشان است. با اجازه دادن به کلاینت که دقیقاً به چه داده هایی نیاز دارد و آن را در یک درخواست واکشی کند، مسئولیت پیمایش و دوخت نمودار را به سرور منتقل می کند. تعبیه انواع و روابط در طراحی اسکیما، می تواند از حل کننده های فیلد برای استخراج داده های لازم و پیمایش کوئری با هر پیچیدگی استفاده کند. این همیشه ارزان نیست و منجر به مشکلات و ابزار اضافی می شود (به عنوان مثال بارگذارهای داده برای رسیدگی به مشکلات n+1)، اما به سرور اجازه می دهد تا از هر تعداد ذخیره داده برای جمع آوری خصوصیات و روابط استفاده کند و کلاینت ها API را نسبت به آنها ناشناس می کند.
چنین جزئیات اجرایی در هسته خود، REST و GraphQL هر دو بر منابع و نمایش آنها متمرکز هستند. این معمولاً با نحوه نمایش موجودیتها در پایگاه دادههایمان همراه است. به یک معنا، این موجودات همیشه در شکل افلاطونی خود وجود دارند، صرف نظر از اینکه ناظر کیست. این می تواند در سیستم های خاصی مشکل ساز باشد، جایی که همه کلاینت ها یکسان ایجاد نمی شوند. داشتن یک اسکیما ایستا یا نموداری که موارد استفاده و نقش های درخواست کننده را در نظر نمی گیرد، می تواند منجر به انواع مشکلات در طراحی شود. فرض کنید مدل کارمند ما دارای یک ویژگی حقوق و دستمزد است که فقط توسط یک نقش حسابدار قابل دسترسی است.
چگونه این را در طراحی API خود نشان داده و پیاده سازی می کنیم؟ آیا ملک را اختیاری می کنیم؟ اگر اختیاری است چگونه بفهمیم فقدان یک مقدار نشان دهنده چیست (هیچ داده ای در فضای ذخیره سازی وجود ندارد یا داده قابل دسترسی برای این درخواست کننده وجود ندارد). آیا لبه دیگری در REST ایجاد می کنیم که خطای HTTP ایجاد کند؟ آیا در GraphQL null می فرستیم یا خطا؟ این ابهام می تواند منجر به پیچیدگی و عدم اطمینان بیشتر در سمت کلاینت شود و باید در مرحله طراحی API مورد توجه قرار گیرد.
رویکرد منبع محور به طراحی API ما باعث می شود که در مورد API های خود از نظر مدل ذخیره سازی داده فکر کنیم. در حالی که در برخی موارد توجیه پذیر است، اگر API خود را خیلی محکم با پایگاه داده خود متصل کنیم، می تواند مشکل ساز باشد. اغلب، ما از مدلهای ORM خود به عنوان مدلهای API خود استفاده میکنیم و به ابزارهایی مانند Hasura، Prisma GraphQL، PostgREST و بسیاری دیگر برای ارتباط مستقیم با پایگاه داده خود از طریق API متکی هستیم. این ابزارها می توانند مقدار زیادی در زمان صرفه جویی کنند، اما خطوط بین لایه های معماری را محو می کنند و کلاینت ها API ما را مستعد تغییرات شکسته می کنند. بسته به نحوه و مکان استفاده از کلاینتها، چنین رویکردی خطری برای یکپارچگی دادهها ایجاد میکند و نیازمند راهحلی برای اصلاح شرایط مسابقه دادههای در حال اجرا و مهاجرتهای اسکیما، تولید و برچسبگذاری نسخههای جدید SDK، و انتشار تغییرات به همه کلاینت ها است. . اگر موتور ذخیره سازی داده از هیچ نسخه ای پشتیبانی نکند، نسخه API مشکل ساز می شود. گذشته از این که کنترل های دسترسی پیچیده تبدیل به یک نقطه دردناک می شوند که ما را ملزم می کند تا برای مدیریت نقش ها و مجوزها به این ابزارها متصل شویم.
از سوی دیگر RPC در مورد منابع و نمایندگی آنها بی نظر است. این به نویسندگان API اجازه می دهد تا از تلاش برای بازتولید مدل داده فاصله بگیرند و در عوض روی موارد استفاده تمرکز کنند.روش ها را می توان بر اساس نیازهای خاص کلاینت ها و اجراکننده های در سیستم تنظیم کرد. بر خلاف REST، که در آن کلاینت به سختی در مورد آنچه دریافت میکند حرفی برای گفتن دارد، و GraphQL، که در آن کلاینت میتواند بدون تفکیک به اندازهای که میخواهد داده درخواست کند (در مقایسه با Cambridge Analytica)، RPC میتواند نیازهای مختلفی را که از برنامههای API مختلف ناشی میشوند، برآورده کند.
سرورهای RPC را میتوان به گونهای تنظیم کرد که بهترینها را از همه دنیا جدا کند.از حلکنندهها تا گرهگشایی گراف گرفته تا میانافزار مسیریابی برای مدیریت جریان درخواست/پاسخ به مسیرهای هممورف و کنترلهای مجوز سطح پرسوجو. RPC API میتواند بر اساس قراردادهای تغییرناپذیر ساخته شود و در طول زمان تکامل یابد تا نیازمندیهای در حال تغییر را به شیوهای سازگار با گذشته برطرف کند. RPC به ما اجازه میدهد تا نگرانیهای API را از نگرانیهای پایداری/ذخیرهسازی داده و سایر جزئیات پیادهسازی جدا کنیم، که منجر به طراحی API مقاومتر در آینده میشود که لازم نیست هر بار که طرح داده تغییر میکند، تغییر کند.
در حالی که GraphQL راه حل خوبی برای سطح API بزرگتر است، جایی که نیازهای کلاینت ها را نمی توان به خوبی پیش بینی کرد، RPC می تواند جایگزین خوبی برای پروژه های کوچکتر باشد - اساساً به جای اینکه کلاینت را وادار به نوشتن کوئری و برشمردن تمام فیلدهایی کند که باید به کلاینت بازگردانده می شود، نوع پاسخ می تواند به صراحت به عنوان بخشی از اسکیما RPC تعریف شود. در اصل، همین امر را می توان در مورد REST نیز گفت، اما این با ماهیت پارادایم که حکم می کند هر منبع باید در مکان یاب شبکه خود در دسترس باشد، مغایرت دارد.
به روز رسانی داده ها
بیایید صادق باشیم، وقتی صحبت از بهروزرسانی دادهها از طریق REST میشود، دردسر است. روشهای مختلف HTTP که قرار است همه چیز را شفاف کنند، در عوض انواع سؤالات را ایجاد میکنند:
چه روشی باید برای حذف نرم یا آرشیو یک منبع استفاده شود؟ آیا باید یک درخواست DELETE با مقداری flag باشد یا درخواست POST با نام action (در واقع آن را یک درخواست RPC می کند؟)، یا باید درخواست PATCH باشد که ویژگی حذف شده یا بایگانی شده یک شی را به روز می کند؟
در مورد به روز رسانی های جزئی چطور؟ از PUT یا PATCH استفاده کنیم؟ در مورد به روز رسانی هایی که دارای عوارض جانبی خاصی هستند، به عنوان مثال. تغییر ایمیل حساب؟ آیا هنوز باید وارد یک درخواست PATCH شوند یا به عنوان POST با نام اقدام ارسال شوند؟
در مورد جزئیات بهروزرسانیهای جزئی که در آن بازیگران خاص فقط مجاز به ایجاد تغییرات خاص هستند، چطور؟ چگونه این منطق را در طراحی REST API خود تقسیم و توصیف کنیم؟
منطقی که واقعاً هیچ منبعی به آن متصل نیست چطور؟ مثلاً مراحل مختلف بازنشانی رمز عبور باید کجا جریان داشته باشد؟ آیا آنها باید در ریشه REST API باشند یا برخی از لبه های API؟
با اسناد تودرتو چه اتفاقی می افتد؟ اگر آن بهروزرسانیهای تودرتو به ایمنی تراکنش نیاز داشته باشند، چه میشود؟
واقعیت این است که هر زمان که یک SDK حول REST API ایجاد میکنید، در نهایت از فراخوانیهای رویه برای توصیف اقدامات مختلفی که انجام میدهد استفاده میکنید، به عنوان مثال. ()deleteUser
یا ()capturePayment
به جای اینکه مستقیماً با متدهای get/post/put/patch/delete
کار کنید. نظر شخصی من این است که REST دیگر برای طراحی برنامههای کاربردی امروزی مناسب نیست. الزامات بسیار پیچیدهتر از آن هستند که بتوان آنها را در طراحی API مبتنی بر منبعمحور تعبیه کرد.
فراخوانهای رویهای برای پیچیدگی برنامههای امروزی مناسبتر هستند، و به طراحان API اجازه میدهند به موارد استفاده خاص بپردازند تا اینکه سعی کنند هر سناریو ممکن را در مجموعه محدودی از نقاط پایانی RESTful تطبیق دهند. تغییر رمز عبور کاربر، مانند تغییر ایمیل کاربر یا انتخاب کاربر در خبرنامه نیست. هر سه ممکن است بخشی از یک منبع باشند، اما به سختی می توانید رمز عبور را به روز کنید و کاربر را در یک درخواست مشترک در خبرنامه کنید. همه این رویه ها دارای عوارض جانبی متمایز هستند و ممکن است نیاز به تأیید اعتبار سفارشی و بررسی مجوز داشته باشند.
RPC به خوبی با اصول CQRS مطابقت دارد. میتوانید فراخوانیهای رویه را بهعنوان کوئری و دستورات در نظر بگیرید، که ملاحظات پروتکل حملونقل را بیربط میکنند. کنترلکنندههای HTTP محمولهها را دریافت و تأیید میکنند، و سپس آنها را به گذرگاههای کوئری و فرمان منتقل میکنند. این کار استفاده مجدد از فراخوانهای رویه را برای هر انتقال دیگر آسان میکند—از خط فرمان تا صف پیام به رباتهای گفتگو و هر چیز دیگری که میتواند فراخوانی رویه را دریافت کند.
جهش های GraphQL در واقع RPC هستند. جدا از یک نام وحشتناک که باعث می شود به جهش های ژنی غیرقابل پیش بینی و قفل شدن فکر کنم، بسیار انعطاف پذیر است. با این حال، این سوال باقی میماند که چه چیزی باید به عنوان جهش در نظر گرفته شود - ارسال ایمیل یا دانلود اسناد رویههایی هستند که واقعاً چیزی را تغییر نمیدهند، بنابراین آیا اصلاً باید بخشی از نمودار باشند؟
نکته مهم دیگری که باید در زمینه طراحی API در نظر گرفت این است که آیا به روز رسانی داده ها همزمان یا ناهمزمان هستند یا خیر. در سیستمهای توزیعشده، همزمانی ممکن است همیشه یک گزینه نباشد، بنابراین باید از قبل برنامهریزی کنید تا راهی برای برقراری ارتباط وقایع پایدار با کلاینت API پیدا کنید، و اینکه چگونه آنها در طرح و مستندات API منعکس میشوند. محل زندگی کلاینت ها شما تعیین می کند که از چه فناوری می توانید برای آن استفاده کنید: وب هوک ها، وب سوکت ها، رویدادهای سمت سرور، اعلان های فشار یا چیز دیگری. در اینجا یک مطالعه جالب در مورد موضوع APIهای رویداد محور ناهمزمان است.
صحبت از سیستم های توزیع شده، ما همچنین باید در نظر بگیریم که استفاده از GraphQL چه پیامدهایی ممکن است داشته باشد. از یک طرف GraphQL برای جمع آوری داده ها از منابع مختلف مناسب است و می تواند به عنوان دروازه ای برای میکروسرویس های شما استفاده شود. با این حال، از سوی دیگر، اختلاط GraphQL با دیگر طرحهای API در سراسر پشته (یا حتی داشتن چندین سرور GraphQL) ممکن است دشوار شود. برای اطمینان از یکپارچگی نمودار، باید در مورد راههایی فکر کنید که میتوانید سرورهای خود را بخیه یا فدرال کنید.
انواع داده ها
وقتی صحبت از طراحی API به میان میآید، در رابطه با انواع دادههای قابل سریالسازی که API قرار است انتقال دهد، باید چند ملاحظات را رعایت کرد:
چگونه می خواهید الزامات سریال سازی را برای انواع اسکالر سفارشی برقرار کنید؟
چگونه می خواهید انواع ترکیبی و یک منطق را بیان کنید، و چگونه این گونه های اتحادیه را متمایز خواهید کرد؟
همه اسکالرها یکسان ایجاد نمی شوند. داشتن یک اسکیما API پر از انواع رشته و اعداد چیزی جز یک کابوس نیست. یک قرارداد باید در مورد الزامات صریح باشد.کلاینتهای API نباید معنی { contact: string } را حدس بزنند، به خصوص اگر اعتبار ضمیمه آن ویژگی باشد:
از شناسه های رشته ای استفاده کنید، به عنوان مثال UUID، ایمیل، IPv6، ISBN و غیره، و در مورد الگوهای رشته سفارشی صریح باشید
از هویت های عدد صحیح استفاده کنید، به عنوان مثال سال، مهر زمانی و غیره، و در مورد محدودههای اعداد صحیح سفارشی صریح باشید
از شناسه های شناور استفاده کنید، به عنوان مثال. Lat، Long و غیره، و در مورد محدوده و دقت برای شناورهای سفارشی صریح باشید
از enums برای بیان لیست های متناهی از گزینه های اسکالر استفاده کنید
یک قالب تاریخ واحد، یعنی رشته تاریخ ISO یا مهر زمانی یونیکس را بپذیرید
هر دو GraphQL و OpenAPI از اصلاحات نوع اسکالر پشتیبانی میکنند، اما نگرانیهای مربوط به سریالسازی را به سرورها و کلاینتها واگذار میکنند، بنابراین حتماً سریالسازی نوع اسکالر سفارشی را در پیادهسازی خود در نظر بگیرید. GraphQL، برخلاف OpenAPI، یک رویکرد بومی برای مستندسازی الزامات اعتبارسنجی در سطح اسکیما ارائه نمیدهد، بنابراین برای برخی از مدیریت خطاهای زمان اجرا آماده باشید. تلاشهایی برای استفاده از دستورالعملهای غیراستاندارد برای بیان محدودیتهای اعتبارسنجی و الزامات قالببندی وجود دارد، اما باید خودتان آنها را ابزار کنید.
یکی دیگر از جنبه های مهم مدل سازی داده ها در طراحی API انواع مختلط است. فرض کنید API شما قرار است فهرستی از موارد موجود در یک کتابخانه را ارائه دهد. ممکن است مجبور شوید با کتابها، مجلات، فیلمها، نوارها، سیدیها و غیره سر و کار داشته باشید که همگی با مدل دادههای خودشان هستند. بسته به طراحی، کلاینت ها API شما ممکن است نیاز داشته باشند بین این مدلها تمایز قائل شوند تا نوع صحیح را برای ایجاد منطق حول آن استنباط کنند.
مشخصات OpenAPI با پشتیبانی داخلی از هر نوع اتحادیه ارائه می شود. از طرف دیگر GraphQL وقتی صحبت از اتحادیه های تبعیض آمیز و انواع مختلط می شود چندان مفید نیست. انواع اسکالر مختلط قابل استفاده نیستند. در حالی که GraphQL از اتحادیه ها پشتیبانی می کند، همه آنها باید رابط یکسانی داشته باشند و نباید فیلدهای همپوشانی با انواع مختلف داشته باشند.
شما باید این محدودیت ها را دور بزنید و همیشه امکان پذیر نیست، بنابراین با یک نوع JSON اسکالر مواجه می شوید، که اینطور نیست. برای ایمنی نوع عالی است.
این محدودیت ها زمانی که با متن ساختاریافته/غنی کار می کنید بیشتر مشهود است. اگر با یک CMS بدون سر کار میکنید که طعمی از متن ساختاریافته را ارائه میکند، به عنوان مثال. PortableText یا Slate، باید به نحوه نمایش این مدل های محتوا در طرح API خود فکر کنید.
فیلتر کردن، مرتب سازی و صفحه بندی
یک نظارت استاندارد هنگام طراحی یک API یک رویکرد استاندارد برای فیلتر کردن، مرتبسازی و صفحهبندی است. همانطور که با یک مجموعه داده کوچک شروع میکنیم، نمیتوانیم آینده را با میلیونها رکورد پیشبینی کنیم، و از این نگرانیها غفلت میکنیم، که بعداً منجر به بازسازی طرح API برای تطبیق با الزامات جدید میشود.
هیچ رویکرد استانداردی برای این نگرانی ها وجود ندارد و API های مختلف به روش های مختلف به آنها رسیدگی می کنند. برای چند ایده به SCIM، OData، CQL نگاهی بیندازید.
وقتی صحبت از فیلترینگ می شود، من از Prisma.io الهام می گیرم. آنها یک رویکرد زیبا برای کوئری از پایگاه داده طراحی کرده اند، اما رویکرد آنها می تواند برای طراحی API نیز قابل اجرا باشد. از آنجایی که این فیلترها قابل سریالسازی هستند، میتوانند به راحتی با درخواستهای HTTP ادغام شوند و طیف وسیعی از نیازمندیها را برای فیلتر کردن مجموعههای داده بزرگ پوشش میدهند.
طراحی صفحه بندی اغلب توسط مدل داده های زیربنایی و همچنین میزان ترافیک دریافتی پایگاه داده تعیین می شود. مجموعههای دادهای که اغلب تغییر میکنند ممکن است از رویکرد مبتنی بر مکاننما سود ببرند، در حالی که دیگران میتوانند با یک رویکرد افست کاملاً خوشحال باشند. در اینجا یک مقاله جالب در این زمینه است.
نکته ای که در اینجا باید به آن توجه کرد این است که چگونه بین درخواست های GET و POST خط می کشیم. این تخصیص جزمی درخواست های GET برای کوئریها و درخواست های POST برای به روز رسانی داده ها که ریشه در REST و اصول اولیه HTTP دارد، کار با فیلترهای بزرگ را به یک دردسر تبدیل می کند. اگر به جنبههای مثبت رویکرد REST نگاه کنید، این روش قابلیت ذخیرهسازی را دارد، اما بیایید صادق باشیم، آخرین باری که برای درخواستهای API خود به حافظه پنهان مرورگر یا CDN اعتماد کردید، چه زمانی بوده است.
این ایده که استفاده از پارامترهای کوئری URL به جای حجم بار بدن به نوعی جهان را به مکانی بهتر تبدیل می کند، صراحتاً منسوخ شده است. سریال سازی URL بد است. طول URL محدود است. بنابراین، بیایید فقط این جزم را کنار بگذاریم و از درخواستهای POST با بارهای JSON برای همه درخواستهای API استفاده کنیم. این تقریباً همان کاری است که کلاینت ها GraphQL در نهایت با محدودیتهای طول کوئری URL مواجه میشوند. به آن در شرایط RPC فکر کنید.شما داده ها را دریافت نمی کنید، اما رویه ای را به سرور ارسال می کنید و از آن پاسخ دریافت می کنید.
محیط و ابزار
تصمیم طراحی API را نمی توان بدون در نظر گرفتن اینکه چه فناوری های سروری برای سرویس دهی به آن استفاده می شود و از چه کلاینت هایی برای دسترسی به آن استفاده می شود، اتخاذ کرد.
از آنجا که REST در کنار پروتکل HTTP تکامل یافته است، بر اساس مجموعه ای از استانداردهای جهانی است. پشته واقعاً کاملاً نامربوط است - همه فنآوریهای سرور و کلاینت میتوانند از طریق HTTP با هم ارتباط برقرار کنند و ابزارهای مربوط به تنظیم یک سرور REST کاملاً غنی است، صرف نظر از اینکه چه زبان برنامهنویسی یا زیرساخت سرور را انتخاب میکنید.
پیاده سازی RPC متفاوت است. چارچوب های مختلفی وجود دارد که ایده های خود را به روی میز می آورد. gRPC وجود دارد که محیط را ناشناس است، اما از نظر ارتباط دو طرفه و استفاده از بافرهای پروتکل کاملاً متفاوت است. tRPC با اجرای تمام پشته TypeScript خود وجود دارد که از REST و GraphQL قرض می گیرد تا یکی از محبوب ترین ابزارهای API را ایجاد کند. ZeroC و UCall و بسیاری دیگر وجود دارد. در نهایت، هیچ چیز شما را از استفاده از ابزار REST برای ساختن APIهای RPC باز نمیدارد. از فرت مغزی که REST است خلاص شوید و فقط نامهای مناسب را به همه پرسشها و دستورات خود بدهید و با استفاده از ابزار استاندارد HTTP آنها را از طریق سیم ارسال کنید. میتوانید رویههای خود را با بخشهای URL، مشابه REST، فضای نام گذاری کنید، اما لازم نیست شناسههای منبع را در ساختار URL وارد کنید و با پیچیدگی روشهای HTTP مقابله کنید.
وقتی صحبت از GraphQL می شود، ایده پشت آن بسیار ساده است:
هر کلاینت داده های مورد نیاز خود را با استفاده از یک زبان کوئری که به طور خاص برای بیان گره های گراف به شکلی بسیار ساده طراحی شده است، تعریف می کند.
سرور کوئری را تجزیه می کند و هر فیلد درخواستی را از طریق حل کننده ای که به گره ریشه دسترسی دارد و می تواند به فروشگاه های مختلف داده و/یا خدمات خارجی برای محاسبه مقدار بازگشتی دسترسی داشته باشد، عبور می دهد.
با این حال، مشکل معرفی یک زبان جدید این است که به یک lexer نیاز دارد، که می تواند کوئری را به شکل استانداردتری تجزیه کند که می تواند توسط فناوری های مختلف سرور هضم شود. از آنجایی که اکثر کلاینت ها API در مرورگر زندگی میکنند، ابزار GraphQL حول نیازهای برنامههای کاربردی جلویی، در یک اکوسیستم جاوا اسکریپت با در نظر گرفتن سازگاری مرورگر، تکامل یافته است. در حالی که GraphQL را به یک تجربه بسیار دلپذیر در بخش جلویی تبدیل میکند، ابزار ارتباط بین سرور و سرور بسیار کم است.از راهاندازی یک کلاینت و کشف کردن حافظه پنهان و سایر نکات ظریف مانند قطعهسازی تا نوشتن و تجزیه پرسوجوهایی که میتوانند توسط گفت کلاینت اگرچه GraphQL برای پروتکل استاندارد HTTP طراحی شده است، مشکل اینجاست که GraphQL به خودی خود استاندارد نیست و ممکن است بهترین انتخاب برای یک طراحی API همه کاره نباشد.
قابلیت مشاهده و مقیاس پذیری
سلامت و عملکرد API معیارهای مهمی هستند که به ما امکان می دهند در مورد طراحی API خود نتیجه گیری کنیم. توانایی مشاهده مداوم این معیارها و واکنش به تاخیر و مسائل مربوط به عملکرد، و همچنین خطاهای زمان اجرا، باید یک عامل انگیزشی در تصمیم گیری ما باشد.
در حالی که API های یک نقطه ورودی سادگی را ارائه می دهند، اما از نظر قابلیت مشاهده و مقیاس پذیری بسیار دردسرساز هستند. پی بردن به سلامت API های GraphQL یا JSON-RPC شما بدون ابزار دقیق کاملاً یک چالش است. در حالی که REST به احتمال زیاد گزارش های دقیقی از منابعی که به چه منابعی دسترسی داشته اند، چند بار و چه مدت طول کشیده است تا درخواست را پردازش کند، در اختیار شما قرار می دهد، دریافت همان آمار برای API هایی که یک نقطه پایانی واحد دارند، کاملا غیرممکن است. در عوض، شما با میلیونها ورودی گزارش برای یک نقطه پایانی روبرو میشوید، و مگر اینکه تلاشی برای ارائه گزارش اضافی با نام عملیات/روش انجام دهید، نمیدانید این درخواستها چه بودهاند. در مورد GraphQL که به کدهای وضعیت HTTP اعتقادی ندارد، اوضاع حتی بدتر است، بنابراین همه درخواستها با 200 کد وضعیت ختم میشوند، بنابراین ردیابی و اشکال زدایی واقعاً هر بار به ماجراجویی تبدیل میشود.
استفاده از یک نقطه ورودی نیز یک مشکل DevOps است. بسته به محیط، ممکن است بخواهید منابع بیشتری را برای نقاط پایانی API که تأخیر بالاتری دارند یا به منابع محاسباتی بیشتری نیاز دارند، تهیه کنید، اما در عوض مجبور هستید کل سرور API را با تمام وابستگیهای آن و تأثیر ردپای حافظه آن مقیاس کنید.
یک نقطه ورودی واحد همچنین حذف مزایای پراکسیهای معکوس را که میتوانند درخواستهای خاص را تغییر مسیر دهند، کمک به ذخیرهسازی، مشاهدهپذیری، تعادل بار، رمزگذاری، کنترل دسترسی و سایر نگرانیها دشوار میکند.
رسیدگی به خطا
کدهای HTTP خیلی عمومی هستند. همانطور که چند سال پیش نوشتم، APIها باید بتوانند مشکلات را به کلاینت ها و متعاقباً کاربران منتقل کنند. هر طراحی API را که انتخاب کردید، مطمئن شوید که یک رویکرد استاندارد برای انتقال خطاها به کلاینت و ایجاد زمینه آنها ایجاد کرده اید.
مدیریت خطای GraphQL یک نعمت و یک نفرین است. از یک طرف، بسیار پرمخاطب است و می تواند خطاها را از حل کننده های فیلد جداگانه گزارش کند. از سوی دیگر، اشکالزدایی و مدیریت خطا را به دردسر میرساند — با ادغام حالتهای موفقیت و خطا در یک پاسخ، تمایز بین خطاهای شبکه، متغیرها، مسائل اعتبارسنجی طرحواره و پاسخهای موفقیت واقعی را برای کلاینت ها سختتر میکند. با توجه به مشخصات، پاسخ های موفقیت جزئی در واقع ممکن است حتی اگر حل کننده های فردی خطاها را گزارش کنند، بنابراین تلاش برای تطبیق مقادیر فیلد تهی با خطاها بسیار دشوار است (مگر اینکه سرور GraphQL از پاسخ های جزئی جلوگیری کند، مانند مورد آپولو). بنابراین، به نحوه برخورد سرور شما با خطاها و اینکه کلاینت ها شما چگونه آنها را درک می کنند، توجه کنید.
همانطور که قبلا ذکر شد، ممکن است بخواهید ابزارهای اضافی را برای کمک به شما در ردیابی خطاها در سیستم هایی که یک نقطه ورودی دارند، استفاده کنید.
نسخه سازی
اینکه آیا API شما نیاز به نسخه سازی دارد یا نه، تا حد زیادی یک مشکل استفاده است. اگر API شما در خانه مصرف میشود و میتوانید APIها را توسعه دهید و در عین حال کلاینت ها را بهروز نگه دارید، ممکن است مشکلی پیش نیاید. اگر API های خود را به صورت خارجی توزیع می کنید، ممکن است بخواهید نوعی از نسخه سازی را در نظر بگیرید.
اگر API شما تا حد زیادی به عنوان یک پروکسی برای مدل پایگاه داده شما وجود داشته باشد، نسخه سازی می تواند تا حدودی یک مشکل حاد باشد. هنگامی که طرح پایگاه داده تغییر می کند، واقعاً راهی برای حفظ سازگاری به عقب وجود ندارد، به خصوص اگر API شما به طور خودکار از مدل های پایگاه داده تولید شده باشد. بنابراین، هنگام انتخاب ابزار خود کمی به آن فکر کنید.
همانطور که قبلا ذکر شد، RPC می تواند گزینه جالبی برای جدا کردن مدل ذخیره سازی از مدل API باشد و به شما انعطاف بیشتری بدهد. شما میتوانید API خود را بدون نیاز به نسخهسازی با معرفی رویههای جدید و منسوخ کردن روشهای قدیمی، تکامل دهید. GraphQL تمایل دارد نسخهسازی را کنار بگذارد و به همان رویکرد متمایل شود.افزودن فیلدهای جدید و منسوخ کردن فیلدهای قدیمی. تفاوت این است که با این حال با RPC کنترل بیشتری روی مسیریابی درخواست دارید و همچنان می توانید نسخه سازی را معرفی کنید. با GraphQL قابلیتهای نسخهسازی شما محدود است——ممکن است لازم باشد ظرف دیگری را بچرخانید که نسخه قدیمیتر نمودار را ارائه میکند. علاوه بر این، شما می خواهید هر از چند گاهی از شر کدهای مرده و قدیمی خلاص شوید، بنابراین برخی از راه های ارتباط با تغییرات شکسته به کلاینت ها یک مزیت خواهد بود.
کنترل های دسترسی
ممکن است بخواهید کمی وقت بگذارید و به شرایط دسترسی فکر کنید. اینکه آیا به سطح مسیر/رویه، سطح منبع یا دسترسی سطح میدان نیاز دارید یا نه، میتواند تأثیر زیادی بر انتخاب طراحی API شما داشته باشد.
اگر بررسی های سطح منبع کافی باشد، هنوز باید در نظر بگیرید که آیا نمایش منبع برای همه کاربران یکسان است یا خیر. اگر به جزئیات بیشتر در کنترلهای دسترسی نیاز دارید، ممکن است لازم باشد راهی برای برقراری ارتباط این الزامات با کلاینت ها پیدا کنید. هر مسیری را که روی سرور در پیش بگیرید (جلوگیری از دسترسی به کل منبع، یا پاک کردن مقدار فیلد، یا ارسال خطا در دسترسی فیلد)، مسئولیت مقابله با پیامدها به عهده کلاینت خواهد بود.
GraphQL در صورت نیاز به دسترسی سطح میدان بسیار مفید است، اگرچه فاقد استانداردهای سطح SDL برای مستندسازی الزامات دسترسی (به غیر از دستورالعمل های غیر استاندارد) است. REST API ممکن است راهحل ایدهآلی برای برنامههایی نباشد که نیاز به دانهبندی سطح بالایی دارند، زیرا از انشعاب بازنمایی منابع جلوگیری میکند. RPC ممکن است در طراحی API با توجه به نیازهای خاص شما انعطاف پذیری ارائه دهد.
نتیجه
طراحی API یک انتخاب دشوار است که باید توسط مرحله کشف محصول هدایت شود، جایی که شما ماهیت ذاتی برنامه خود و سایر بازیگران سیستم را درک می کنید.
از REST استفاده کنید اگر:
- شما در حال ساخت یک API برای تعامل با پایگاه داده خود با استفاده از عملیات CRUD هستید
- مدل داده شما روی روابط سنگین نیست
- شما تعداد محدودی از نقش های کاربر و الزامات دسترسی دارید
از GraphQL استفاده کنید اگر:
- شما در حال ساخت یک دروازه برای اتصال چندین منبع داده یا خدمات هستید
- مدل داده شما بر روی روابط سنگین است و یک نمودار را نشان می دهد
- شما نیازهای دسترسی با دانه بندی بالا دارید
- شما در حال ساختن یک API هستید که بیشتر برای برنامه های کاربردی فرانت اند هدف گذاری شده است
- شما تعداد زیادی کلاینت API با نیازهای داده متفاوت دارید
از RPC استفاده کنید اگر:
- شما می خواهید در نامگذاری و قراردادهای مسیریابی انعطاف پذیر باشید
- شما می خواهید API های معنایی و خوانا بسازید
- شما باید به موارد استفاده واگرا، فرار یا پیچیده بپردازید
- خدمات شما خارج از پروتکل HTTP تعامل دارند
- شما می خواهید یک API مقاوم در برابر آینده بسازید که از نگرانی های پیاده سازی و ذخیره سازی داده جدا شده باشد
اگر نظر شخصی من را بخواهید، فکر می کنم آینده RPC است. من GraphQL را برای توسعه فرانتاند دوست دارم، اما فکر نمیکنم راهحل ایدهآلی برای ارتباطات متقابل باشد، و وقتی انگشتان پا را در آن فرو کنید، پایتان را گاز میگیرد و استدلال کردن دربارهاش را بسیار سختتر میکند. به هر حال، از GraphQL به عنوان یک پروکسی برای مدل خواندن خود استفاده کنید، اما آن را به عنوان بخشی از خدمات داخلی خود در ترکیب قرار ندهید. و در مورد REST، بگذارید استراحت کند.