7 دقیقه مطالعه

REST، GraphQL یا tRPC؛ انتخاب را از شکل کلاینت شروع کن

Mehdi Rezaei
Mehdi
نویسنده

REST، GraphQL یا tRPC؛ انتخاب را از شکل کلاینت شروع کن

بحث API اغلب شبیه انتخاب برند است. REST کهنه، GraphQL مدرن، tRPC باحال. این تقسیم به درد کد نمی‌خورد. سؤال واقعی این است که مصرف‌کننده کیست، قرارداد چطور نسخه می‌خورد، و کش را کجا می‌خواهی.

من هر سه را در محصول‌های Next.js و Node دیده‌ام. هیچ‌کدام برنده همیشگی نیست. برنده همانی است که شش ماه بعد، موقع حادثه، هنوز می‌توانی شکل درخواست را برای یک نفر دیگر توضیح بدهی.

REST هنوز پیش‌فرض مرز بیرونی است

REST یعنی منبع، فعل HTTP، و پاسخ قابل پیش‌بینی. بی‌وضعیت بودن دردسر احمقانه نیست؛ هر درخواست خودش را کامل حمل می‌کند و مقیاس افقی ساده‌تر می‌شود. کش HTTP، CDN، و `curl` هنوز ابزار روزمره‌اند.

جایی REST می‌درخشد: API عمومی، موبایل و وب با تیم جدا، وب‌هوک، و هر جا که مصرف‌کننده را کنترل نمی‌کنی. یکپارچگی با شریک بیرون با GraphQL سفارشی معمولاً گران‌تر از چند endpoint تمیز است.

ضعف آشناست. over-fetching و under-fetching. لیست کارت پست یا کل سند CMS را برمی‌گرداند، یا برای یک صفحه چهار درخواست آبشاری می‌زند. این ضعف پروتکل نیست؛ ضعف طراحی منبع است. endpointهای باریک برای صفحه، به‌جای منبع‌های ریز همه‌منظوره، خیلی از این درد را بدون GraphQL کم می‌کند. اسمش را BFF بگذار یا نگذار؛ شکلش مهم است.

نسخه‌گذاری REST وقتی شل باشد به جنگل endpoint ختم می‌شود. قرارداد را صریح کن. فیلد جدید را additive نگه دار. شکستن را با نسخه یا هدر اعلام کن، نه با غیب‌شدن یک کلید.

GraphQL وقتی شکل داده دست کلاینت است

یک endpoint، کوئری که خودش فیلدها را نام می‌برد. برای چند کلاینت با نیاز متفاوت، داشبورد متراکم، و گراف رابطه‌ای که REST را به انفجار درخواست می‌کشاند، این مدل منطقی است. Query برای خواندن، mutation برای نوشتن، و subscription اگر واقعاً جریان زنده لازم است نه برای هر لیست.

هزینه را دست‌کم نگیر. پیچیدگی سرور، N+1 پشت resolver، عمق کوئری، و کش که دیگر URL ساده نیست. persisted query و محدودیت عمق و پیچیدگی جزو طراحی‌اند، نه بهینه‌سازی سال دوم. بدون آن، یک کلاینت کنجکاو می‌تواند گران‌ترین join را در پروداکشن کشف کند.

اگر تیم schema را مثل محصول نسخه نمی‌دهد و ابزار observability روی resolver ندارید، GraphQL تبدیل می‌شود به REST بد با یک URL. ابزار خوب (کدجن، schema lint، tracing) این را قابل زندگی می‌کند. ابزار جای مرز دامنه را نمی‌گیرد.

احراز هویت و مجوز باید روی فیلد یا روی لایه داده باشد، نه فقط روی وجود کوئری. پنهان کردن دکمه در UI مجوز نیست. این در GraphQL خطرناک‌تر است چون کلاینت خودش شکل خواندن را می‌سازد.

tRPC وقتی هر دو طرف مال خودتان است

tRPC قرارداد را از تایپ TypeScript درمی‌آورد. فرانت و بک در یک مونوریپو، بدون codegen جدا، بدون schema دوم. برای اپ Next.js که مصرف‌کننده عمومی ندارد، سرعت توسعه واقعی است. ورودی و خروجی را با Zod یا مشابه ببند تا «تایپ» فقط آرزوی کامپایلر نباشد.

مرز tRPC مرز محصول داخلی است. موبایل native، شریک، یا وب‌هوک عمومی را به آن گره نزن مگر اینکه عمداً یک لایه OpenAPI از همان روتر بیرون بدهی. وگرنه قفل می‌شوی به کلاینت خودت.

کش HTTP رایگان REST را نداری. باید سیاست کش را خودت روی React Query یا مسیر سرور بنویسی. این بد نیست اگر از اول بدانی. بد است اگر انتظار داشته باشی CDN مثل GETهای REST رفتار کند.

خطاها را هم مثل API طراحی کن. throw آزاد داخل procedure، برای کلاینت یعنی پیام تصادفی. کد خطا، شکل ثابت، و اینکه کدام خطا قابل retry است را از روز اول مشخص کن.

چطور انتخاب را روی کاغذ بیاور

سه سؤال کافی است.

اول: چند مصرف‌کننده مستقل داری؟ یکی و هر دو TypeScript، tRPC اغلب ارزان‌تر است. چند کلاینت با شکل داده متفاوت، GraphQL یا BFFهای REST جدا. دنیای بیرون، REST.

دوم: خواندن‌ها چقدر هم‌پوشانی دارند؟ اگر هر صفحه ترکیب تازه‌ای از همان گراف می‌خواهد، GraphQL یا یک لایه query داخلی. اگر صفحه‌ها منبع پایدار دارند، REST باریک.

سوم: کجا باید کش شود و چطور باطل شود؟ URL و هدر با REST راحت‌تر است. GraphQL و tRPC باطل‌سازی را به برچسب و قرارداد خودت گره می‌زنند. اگر تیم هنوز داستان cache را برای REST ساده خراب می‌کند، GraphQL اوضاع را بهتر نمی‌کند.

روی Next.js App Router این انتخاب با مرز سرور قاطی می‌شود. Server Component می‌تواند مستقیم به دیتابیس یا SDK داخلی حرف بزند و اصلاً API عمومی نسازد. tRPC یا REST را برای جایی بگذار که کلاینت، موبایل، یا شخص ثالث واقعاً لازم دارد. ساختن سه سبک موازی برای یک فرم، معماری نیست.

ترکیب، به شرط مرز مشخص

خیلی از کدبیس‌های سالم مخلوط‌اند. داده عمومی و وب‌هوک روی REST. اپ داخلی روی tRPC. یک گراف مشخص، مثلاً کاتالوگ پیچیده، روی GraphQL. مشکل از مخلوط بودن نیست؛ مشکل از این است که هیچ‌کس نداند درخواست جدید باید کجا بنشیند.

یک قانون سرانگشتی که برای من کار کرده: اگر شکل پاسخ را صفحه تعیین می‌کند و تیم مالک هر دو طرف است، نزدیک کد بمان، tRPC یا تابع سرور. اگر شکل پاسخ را قرارداد بلندمدت تعیین می‌کند، REST یا schemaی GraphQL نسخه‌دار.

قرارداد خطا و نسخه را همان روز اول بنویس

انتخاب سبک اگر شکل خطا سلیقه‌ای باشد، سه ماه بعد فرق نمی‌کند. یک بدنه خطا کافی است: کد پایدار، پیام قابل‌نمایش برای کاربر، و جزئیات فنی فقط در لاگ. کلاینت نباید رشته انگلیسی stack را پارس کند.

retry را به کد گره بزن، نه به حس توسعه‌دهنده. خطای اعتبار ۴۰۰ را دوباره نزن. قطع شبکه و ۵۰۳ را با سقف و فاصله بزن. mutation را بدون کلید یکتا retry نکن؛ وگرنه فرم ثبت‌نام دو کاربر می‌سازد و بحث GraphQL و REST بی‌معنی می‌شود.

نسخه را وقتی بشکن که مصرف‌کننده را همزمان نمی‌توانی منتشر کنی. فیلد اختیاری جدید نسخه نیست. حذف فیلد یا تغییر معنی آن نسخه است. در tRPC این وسوسه زیاد است که «هر دو طرف مال ماست، همین امروز هر دو را عوض می‌کنیم». تا وقتی موبایل یا job پس‌زمینه هم همان تایپ را مصرف می‌کند، این فرض غلط است.

مستند زنده را از روی schema یا روتر بساز، نه از ویکی جدا که هفته دوم کهنه می‌شود. OpenAPI برای REST، schema برای GraphQL، و تایپ صادرشده برای tRPC. اگر ابزار مستند از کد عقب ماند، کسی به آن اعتماد نمی‌کند و قرارداد دوباره شفاهی می‌شود.

یک مثال تصمیم، بدون تعصب ابزار

فرض کن یک محصول Next.js داری با داشبورد تیمی و یک API برای شریک که فاکتور را می‌خواند. داشبورد هر صفحه ترکیب متفاوتی از کاربر، سازمان و وضعیت اشتراک می‌خواهد. شریک فقط یک منبع فاکتور با فیلتر تاریخ می‌خواهد و باید با کش CDN و امضای درخواست زندگی کند.

اینجا دو مرز سالم‌تر از یک سبک واحد است. خواندن داشبورد می‌تواند پشت سرور خودش بماند یا با tRPC داخل مونوریپو بیاید. فاکتور شریک REST نسخه‌دار می‌ماند، با فیلدهای پایدار و صفحه‌بندی صریح. کشیدن شریک به tRPC یا باز کردن کل گراف GraphQL به بیرون، هزینه مجوز و نسخه را یکجا بالا می‌برد.

اگر شش ماه بعد کلاینت موبایل آمد و همان گراف داشبورد را با شکل دیگری خواست، آن وقت لایه GraphQL یا یک BFF جدا معنی پیدا می‌کند. زودتر ساختنش یعنی schemaیی که هنوز محصول نشده را نگهداری می‌کنی.

پرسش‌های کوتاه

می‌شود بعداً مهاجرت کرد؟ بله، اگر مرز دامنه تمیز باشد. مهاجرت سخت وقتی UI مستقیم به شکل عجیب پاسخ چسبیده.

GraphQL برای اپ کوچک Next.js لازم است؟ معمولاً نه. تا وقتی کلاینت دوم با نیاز واقعاً متفاوت نیامده، هزینه schema را نمی‌خرد.

tRPC جای اعتبارسنجی است؟ نه. تایپ مسیر را همگام می‌کند. قانون کسب‌وکار و ورودی کثیف هنوز schema اعتبارسنجی می‌خواهد.

Share this article