# قراردادهای تایپ‌شده بین بک‌اند و کلاینت: چرا اسکیمای مشترک باگ‌های یکپارچه‌سازی را کم می‌کند

- Published: 2026-09-30
- Reading time: 9 min
- Tags: API Design, Type Safety, Backend

«در Postman که کار می‌کرد» جمله‌ای است که تقریباً هر تیمی حداقل یک‌بار گفته و بعدش فهمیده مشکل واقعی جای دیگری بوده. قرارداد تایپ‌شده بین بک‌اند و کلاینت این کلاس کامل از باگ‌ها را از دسته‌بندی «باگ زمان اجرا» به «خطای زمان کامپایل» منتقل می‌کند.

«در Postman که کار می‌کرد» یکی از آشناترین جمله‌های هر تیمی است که بک‌اند و کلاینت را جدا از هم توسعه می‌دهد. Postman یک درخواست دستی می‌فرستد، یک پاسخ می‌گیرد، و توسعه‌دهنده با چشمش نگاه می‌کند و می‌گوید «بله، این درست است». مشکل این نیست که Postman دروغ می‌گوید؛ مشکل این است که این تأیید فقط دربارهٔ همان یک درخواست خاص است، نه دربارهٔ قراردادی که کلاینت واقعی — با فرض‌هایی دربارهٔ نوع هر فیلد، اختیاری یا اجباری بودنش، و شکل دقیق یک آرایه یا یک شیء تودرتو — واقعاً روی آن تکیه می‌کند.

### جایی که «در Postman کار می‌کرد» می‌شکند

فاصلهٔ بین «یک پاسخ نمونه که چشم توسعه‌دهنده آن را تأیید کرده» و «قراردادی که کلاینت واقعاً به آن متکی است» دقیقاً همان جایی است که بیشتر باگ‌های یکپارچه‌سازی زندگی می‌کنند. یک فیلد که در ۹۹ درصد پاسخ‌ها وجود دارد اما در یک حالت خاص (مثلاً کاربری که هنوز پروفایلش را کامل نکرده) null یا غایب است. یک عدد که بک‌اند به‌عنوان رشته برمی‌گرداند چون از یک کتابخانهٔ سریال‌سازی پیش‌فرض استفاده کرده، درحالی‌که کلاینت فرض کرده عدد است. یک آرایه که در حالت خالی، به‌جای [] مقدار null برمی‌گردد. هیچ‌کدام از این‌ها در یک تست دستی با Postman روی داده‌ٔ نمونهٔ معمول ظاهر نمی‌شوند، چون داده‌ٔ نمونه معمولاً «حالت خوب» است، نه حالت لبه‌ای. مشکل عمیق‌تر این است که این نوع خطا معمولاً زمان اجرا خودش را نشان می‌دهد، نه زمان توسعه — دقیقاً وقتی که رفع آن گران‌ترین حالت ممکن را دارد: بعد از استقرار، وقتی کاربر واقعی به آن حالت لبه‌ای برخورد کرده. یک قرارداد تایپ‌شدهٔ مشترک، این کلاس کامل از خطاها را از دستهٔ «باگ زمان اجرا که کاربر پیدایش می‌کند» به دستهٔ «خطای زمان کامپایل که قبل از merge شدن دیده می‌شود» منتقل می‌کند. این تفاوت، تفاوت بین یک ساعت زمان توسعه و یک ساعت رفع اشکال ساعت دو نیمه‌شب است.

### تایپ تولیدشده در برابر تایپ دستی

وقتی تیمی تصمیم می‌گیرد این مشکل را حل کند، معمولاً بین دو رویکرد انتخاب می‌کند: تایپ‌های کلاینت را دستی بنویسد و با مستندات API هماهنگ نگه دارد، یا تایپ‌ها را از یک منبع حقیقت واحد (مثلاً یک تعریف اسکیمای بک‌اند) به‌صورت خودکار تولید کند. تایپ دستی سریع‌تر شروع می‌شود و برای یک API کوچک و پایدار کاملاً معقول است. اما هزینه‌اش با گذر زمان ظاهر می‌شود، نه در روز اول: هر تغییری در بک‌اند باید کسی به‌یاد بیاورد که باید در تایپ کلاینت هم دستی اعمال شود، و «به‌یاد آوردن» دقیقاً همان چیزی است که فرایندهای انسانی در آن ضعیف‌اند. تایپ تولیدشده این حلقهٔ یادآوری دستی را حذف می‌کند: تایپ کلاینت مستقیماً از همان تعریفی می‌آید که بک‌اند از آن پیروی می‌کند، و اگر بک‌اند تغییر کند، تولید مجدد تایپ‌ها فوراً نشان می‌دهد کجا کد کلاینت دیگر با قرارداد جدید جور درنمی‌آید — نه ماه‌ها بعد، بلکه در همان لحظه‌ای که کد تولید می‌شود یا کامپایل انجام می‌شود. هزینهٔ تایپ تولیدشده جای دیگری است: به یک ابزار تولید، یک مرحلهٔ اضافه در فرایند بیلد، و انضباطی نیاز دارد که کسی هرگز تایپ تولیدشده را دستی ویرایش نکند (چون دفعهٔ بعد که تولید دوباره اجرا شود، آن ویرایش دستی بی‌سروصدا از بین می‌رود). برای تیم‌های کوچک با API بسیار ساده، این هزینه ممکن است از فایده‌اش بیشتر باشد. برای تیمی که API‌اش رشد می‌کند و بیش از یک نفر روی بک‌اند و کلاینت کار می‌کنند، تجربهٔ عمومی صنعت این است که نقطهٔ سربه‌سر خیلی زودتر از آن‌چه تصور می‌شود می‌رسد.

### تغییر قرارداد در مرز یک انتشار

جایی که این بحث از یک ترجیح سبکی به یک مسئلهٔ واقعی درستی تبدیل می‌شود، مرز یک انتشار (release) است. بک‌اند و کلاینت تقریباً هرگز دقیقاً هم‌زمان مستقر نمی‌شوند — یک اپ موبایل ممکن است روزها یا هفته‌ها طول بکشد تا همهٔ کاربران آن را به‌روزرسانی کنند، درحالی‌که بک‌اند می‌تواند چند بار در روز مستقر شود. این یعنی همیشه یک پنجرهٔ زمانی وجود دارد که نسخهٔ قدیمی کلاینت با نسخهٔ جدید بک‌اند صحبت می‌کند، یا برعکس. یک قرارداد تایپ‌شده به‌تنهایی این مسئله را حل نمی‌کند؛ فقط آن را قابل‌مشاهده می‌کند. مسئولیت واقعی روی قوانین سازگاری قرار می‌گیرد که تیم باید آگاهانه انتخاب کند: فیلد جدید اضافه‌شده باید اختیاری باشد تا کلاینت قدیمی که از وجودش خبر ندارد نشکند؛ فیلدی که دیگر استفاده نمی‌شود باید حذف نشود، بلکه به‌عنوان منسوخ علامت بخورد و برای یک دورهٔ مشخص هم‌زمان با فیلد جدید نگه داشته شود؛ و تغییر نوع یک فیلد موجود (مثلاً از رشته به عدد) تقریباً همیشه باید به‌عنوان یک فیلد کاملاً جدید مدل‌سازی شود، نه یک تغییر درجا. این‌ها قوانین نسخه‌بندی API هستند، و قرارداد تایپ‌شده فقط ابزاری است که این قوانین را قابل‌اجرا و قابل‌بررسی می‌کند — بدون آن ابزار، همین قوانین هنوز باید رعایت شوند، اما نقض‌شان تا وقتی کاربر واقعی به آن برخورد کند دیده نمی‌شود.

### تست قرارداد

لایهٔ آخری که این تصویر را کامل می‌کند، تست قرارداد (contract testing) است. تست واحد معمولی بک‌اند تأیید می‌کند که منطق داخلی سرویس درست کار می‌کند؛ تست end-to-end تأیید می‌کند که کل سامانه در یک محیط شبیه‌سازی‌شده درست کار می‌کند. هیچ‌کدام از این‌ها مستقیماً این سؤال را جواب نمی‌دهد: «اگر کلاینتی که همین الان در دست کاربران است با نسخهٔ بعدی بک‌اند که قرار است مستقر شود صحبت کند، چه اتفاقی می‌افتد؟» تست قرارداد دقیقاً همین شکاف را پر می‌کند: مصرف‌کننده (کلاینت) انتظاراتش از قرارداد را به‌صورت قابل‌اجرا مستند می‌کند، و تولیدکننده (بک‌اند) پیش از استقرار هر تغییری، این انتظارات را به‌عنوان بخشی از فرایند build خودش اجرا می‌کند. این با تایپ تولیدشده فرق دارد: تایپ‌ها تضمین می‌کنند شکل داده در زمان کامپایل درست است، اما نمی‌توانند تضمین کنند رفتار واقعی سرویس (مثلاً «این فیلد در این حالت خاص همیشه پر است») هنوز همان رفتاری است که کلاینت رویش حساب باز کرده. تست قرارداد این لایهٔ رفتاری را می‌پوشاند، جایی که تایپ به‌تنهایی نمی‌تواند.

### کدام لایه واقعاً مالک اسکیما است

پرسشی که تیم‌ها معمولاً دیر مطرحش می‌کنند این است: اسکیما از کجا شروع می‌شود؟ دو الگوی رایج وجود دارد. در الگوی اول، بک‌اند مالک اسکیماست — تعریف مسیرها، ورودی‌ها و خروجی‌ها در کد بک‌اند نوشته می‌شود و تایپ‌های کلاینت از روی همان تولید می‌شوند. در الگوی دوم، یک تعریف اسکیمای مستقل (یک فایل قرارداد که نه به بک‌اند تعلق دارد نه به کلاینت) منبع حقیقت است، و هم بک‌اند و هم کلاینت از روی همان تولید می‌شوند. الگوی اول ساده‌تر است و برای تیمی که یک بک‌اند و یک یا دو کلاینت دارد کاملاً کافی است — چون فقط یک جهت تولید وجود دارد و نیازی به هماهنگی بین دو کدبیس مستقل نیست. الگوی دوم وقتی ارزشش را نشان می‌دهد که بیش از یک تیم بک‌اند وجود دارد، یا وقتی همان قرارداد باید توسط زبان‌های برنامه‌نویسی مختلف مصرف شود (مثلاً یک بک‌اند به یک زبان و چند کلاینت به زبان‌های دیگر). انتخاب اشتباه بین این دو الگو معمولاً به این شکل ظاهر می‌شود: تیمی که الگوی دوم را برای یک بک‌اند و یک کلاینت ساده انتخاب کرده، بیشتر وقتش را صرف نگه‌داشتن یک فایل قرارداد مجزا می‌کند تا صرف کاری که واقعاً برایش پول می‌گیرد.

### پذیرش تدریجی روی یک API قدیمی

اکثر تیم‌ها این تصمیم را روی یک پروژهٔ تازه نمی‌گیرند؛ روی یک API چند ساله‌ای می‌گیرند که از اول بدون قرارداد تایپ‌شده نوشته شده و حالا صدها اندپوینت دارد. بازنویسی کامل یک‌جا تقریباً همیشه غیرعملی است و معمولاً هرگز به پایان نمی‌رسد. راهی که در عمل جواب می‌دهد، شروع از پرترافیک‌ترین یا پرخطاترین اندپوینت‌هاست — همان‌هایی که تاریخچهٔ باگ‌های یکپارچه‌سازی‌شان طولانی‌ترین است — و افزودن قرارداد تایپ‌شده اندپوینت‌به‌اندپوینت، درحالی‌که بقیهٔ API همچنان بدون تایپ باقی می‌ماند تا نوبتش برسد. این یعنی برای مدتی هر دو رویکرد هم‌زمان در یک کدبیس وجود دارند، و این وضعیت باید آگاهانه پذیرفته شود، نه به‌عنوان یک بی‌نظمی موقت که «بعداً جمعش می‌کنیم» بلکه به‌عنوان مسیر واقعی مهاجرت. نشانه‌ای که این مهاجرت درست پیش می‌رود این است که هر اندپوینت تازه تایپ‌شده، بلافاصله یک یا دو باگ قدیمی و ساکت را آشکار می‌کند — فیلدی که همیشه فرض می‌شد اجباری است اما در یک حالت خاص نبوده، یا نوعی که در مستندات یک چیز نوشته شده بود و در عمل چیز دیگری برمی‌گشت. این آشکارشدن، خودش دلیل خوبی است برای ادامه‌دادن مهاجرت به‌جای متوقف‌کردنش پس از چند اندپوینت اول.

### نتیجه‌گیری عملی

هیچ‌کدام از این ابزارها — تایپ مشترک، تولید خودکار، تست قرارداد — جایگزین طراحی خوب API نمی‌شوند. آن‌ها فقط فاصلهٔ زمانی بین «قرارداد شکسته شد» و «کسی متوجه شد» را کوتاه می‌کنند. برای تیمی که بک‌اند و کلاینتش را جدا از هم و با سرعت‌های متفاوت مستقر می‌کند، این کوتاه‌شدن فاصله دقیقاً همان چیزی است که تفاوت بین یک انتشار آرام و یک هفتهٔ پر از تیکت پشتیبانی را می‌سازد.


---

Source: https://larsima.com/insights/typed-contracts-backend-client
Organisation: LARSIMA (شرکت فن‌آوران توسعه لار سیما), registered in Iran, no. 318515, since 2007-12-02.
