پست

طراحی API در مصاحبه‌های طراحی سیستم

راهنمای عملی طراحی API برای مصاحبه‌های طراحی سیستم شامل انتخاب بین REST، GraphQL و RPC، مدل‌سازی منابع، صفحه‌بندی، نسخه‌سازی، احراز هویت و Rate Limiting

طراحی API در مصاحبه‌های طراحی سیستم

طراحی API

با طراحی API برای مصاحبه‌های طراحی سیستم آشنا شوید.

در مصاحبه‌های طراحی سیستم باید مشخص کنید کلاینت‌ها چطور با سیستم شما تعامل می‌کنند. این کار بخشی از مرحله API در چارچوب تحویل (Delivery Framework) است.

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

قبل از ورود به جزئیات، یک چیز را روشن کنم. بیشتر مصاحبه‌گرها عمیقاً به کامل بودن طراحی API شما اهمیت نمی‌دهند. می‌خواهند ببینند می‌توانید یک API معقول طراحی کنید و بعد سراغ بخش‌های پیچیده‌تر سیستم بروید.

با این حال، اگر برای نقش‌های فرانت‌اند یا محصول مصاحبه می‌دهید، طراحی API مهم‌تر است، چون هر روز با API کار می‌کنید. در نقش‌های جونیور هم از شما انتظار کمتری برای طراحی سیستم‌های توزیع‌شده دارند، پس ممکن است زمان بیشتری در مصاحبه صرف API شود.

انواع API

در مصاحبه معمولاً بین سه پروتکل اصلی API انتخاب می‌کنید.

  • REST (Representational State Transfer): REST از متدهای استاندارد HTTP (GET، POST، PUT، DELETE) برای کار با منابعی که با URL شناسایی می‌شوند استفاده می‌کند. برای عملیات استاندارد CRUD در برنامه‌های وب و موبایل، REST طبیعی با عملیات پایگاه داده و معنای HTTP نگاشت می‌شود. به همین دلیل پروتکل غالب بیشتر سرویس‌های وب است و باید انتخاب پیش‌فرض شما باشد.
  • GraphQL: برخلاف endpointهای ثابت REST، GraphQL یک endpoint واحد با زبان کوئری دارد که کلاینت‌ها با آن دقیقاً مشخص می‌کنند چه داده‌ای لازم دارند. اپلیکیشن موبایلی را در نظر بگیرید که فقط اطلاعات پایه کاربر را می‌خواهد، در مقابل داشبورد وب که تحلیل‌های کامل نشان می‌دهد. در REST یا باید چند endpoint بسازید یا کلاینت‌ها را مجبور کنید بیش از نیازشان داده بگیرند. اما GraphQL به هر کلاینت اجازه می‌دهد دقیقاً آنچه را لازم دارد در یک کوئری بخواهد. اگر مصاحبه‌گر به «واکشی انعطاف‌پذیر داده» اشاره کند یا درباره پرهیز از over-fetching و under-fetching صحبت کند، یعنی می‌خواهد GraphQL را در نظر بگیرید.
  • RPC (Remote Procedure Call): پروتکل‌های RPC مثل gRPC برای ارتباط کارآمد بین سرویس‌ها از serialization باینری و HTTP/2 استفاده می‌کنند. REST همه‌چیز را منبع می‌بیند، اما RPC اجازه می‌دهد برحسب اقدام‌ها و رویه‌ها فکر کنید. مثلاً وقتی سرویس کاربر باید سریع با سرویس احراز هویت بررسی کند که مجوزی وجود دارد، یک فراخوانی RPC مثل checkPermission(userId, resource) طبیعی‌تر از مدل کردن آن به‌صورت یک منبع REST است. اگر مصاحبه‌گر صریحاً از میکروسرویس‌ها یا APIهای داخلی حرف بزند، RPC را برای آن اتصال‌های پرکارایی در نظر بگیرید. وقتی کارایی حیاتی است از RPC استفاده کنید. (برای جزئیات بیشتر پروتکل‌ها، «مبانی شبکه» را ببینید.)

به‌طور پیش‌فرض روی REST بمانید، مگر دلیل مشخصی داشته باشید. REST خوب فهمیده شده، ابزارهای عالی دارد و برای ۹۰٪ موارد جواب می‌دهد. اگر مطمئن نیستید، فقط بگویید «از REST API استفاده می‌کنم» و جلو بروید.

برای قابلیت‌های بلادرنگ مثل اعلان‌ها، چت یا به‌روزرسانی زنده، به پروتکل‌های دیگری مثل WebSocket یا Server-Sent Events نیاز دارید. این‌ها APIهای سنتی نیستند و اتصال‌های پایدارند. اما دانستنشان مهم است. درباره همه آن‌ها می‌توانید در الگوی «به‌روزرسانی‌های بلادرنگ» مطالعه کنید.

بیایید هر کدام را مرور کنیم و ببینیم در مصاحبه چه چیزی اهمیت دارد.

REST

چون REST انتخاب پیش‌فرض شماست، بیشتر وقتمان را صرف فهمیدن طراحی RESTful API می‌کنیم که در مصاحبه‌های طراحی سیستم خوب جواب می‌دهد.

مدل‌سازی منابع

پایه طراحی خوب REST API این است که منابع را درست شناسایی کنید. اگر چارچوب تحویل را دنبال کرده باشید، منابع شما همان موجودیت‌های اصلی (core entities) هستند.

Ticketmaster را مثال بزنید. موجودیت‌های اصلی شما ممکن است رویداد (event)، سالن (venue)، بلیت (ticket) و رزرو (booking) باشند. این‌ها طبیعی به منابع REST نگاشت می‌شوند.

1
2
3
4
5
6
GET /events                    # Get all events
GET /events/{id}               # Get a specific event
GET /venues/{id}               # Get a specific venue
GET /events/{id}/tickets       # Get available tickets for an event
POST /events/{id}/bookings     # Create a new booking for an event
GET /bookings/{id}             # Get a specific booking

مهم‌تر از همه این‌که منابع REST باید نماینده «چیزها» در سیستم باشند، نه «کارها». به‌جای فکر کردن به کارهایی که کاربر انجام می‌دهد (مثل «رزرو کن» یا «بخر»)، به چیزهایی فکر کنید که در سیستم وجود دارند (رویدادها، سالن‌ها، بلیت‌ها، رزروها).

نام منابع باید همیشه جمع باشد، مثلاً bookings، events و tickets. بیشتر مصاحبه‌گرها به این موضوع اهمیت نمی‌دهند، اما بعضی‌ها می‌دهند و رعایتش هم آسان است.

برای مدیریت رابطه بین منابع دو رویکرد اصلی دارید. وقتی رابطه والد و فرزند روشنی وجود دارد، مثل /events/{id}/tickets برای همه بلیت‌های یک رویداد مشخص، می‌توانید منابع را تو در تو (nested) کنید. یا می‌توانید منابع را مستقل نگه دارید و برای فیلتر کردن از query parameter استفاده کنید، مثل /tickets?event_id=123.

تفاوت اصلی در این است که رابطه الزامی است یا اختیاری. وقتی مقدار الزامی است از path parameter (یا منابع تو در تو) استفاده کنید. مثلاً /events/{id}/tickets که همیشه باید بلیت‌های یک رویداد مشخص را بخواهید. وقتی فیلتر اختیاری است از query parameter استفاده کنید. مثلاً /tickets?event_id=123&section=VIP که می‌توانید همه بلیت‌ها را بگیرید، یا بلیت‌ها را بر اساس رویداد، یا بر اساس رویداد و بخش فیلتر کنید.

این تفاوت در مصاحبه کمک می‌کند سریع انتخاب درستی بکنید. اگر رابطه همیشه برای معنادار بودن کوئری لازم است، از path parameter استفاده کنید. اگر فیلتر اختیاری است و یکی از چند فیلتر ممکن است، از query parameter استفاده کنید. مصاحبه‌گر بیشتر به این اهمیت می‌دهد که منابع درست را شناسایی کنید، نه ساختار کامل URL. با این حال، نشان دادن این درک شهود خوب شما در طراحی API را نشان می‌دهد.

متدهای HTTP

بعد از شناسایی منابع باید تصمیم بگیرید کلاینت‌ها چطور با آن‌ها تعامل کنند. HTTP مجموعه‌ای از متدها (فعل‌ها) دارد که طبیعی به عملیات رایج نگاشت می‌شوند. دانستن زمان استفاده از هر کدام برای مصاحبه خیلی مهم است.

  • GET: برای بازیابی داده بدون تغییر دادن چیزی. برای گرفتن جزئیات رویداد از /events/{id} و برای فهرست رویدادها از /events استفاده کنید.
  • POST: منبع جدید می‌سازد. وقتی کاربری بلیت رزرو می‌کند، با جزئیات رزرو در بدنه درخواست به /events/{id}/bookings POST می‌کنید. سرور یک شناسه اختصاص می‌دهد و رزرو تازه‌ساخته‌شده را برمی‌گرداند. POST نه safe است و نه idempotent. یعنی چند بار فراخوانی آن چند رزرو ایجاد می‌کند.
  • PUT: کل منبع را با چیزی که می‌فرستید جایگزین می‌کند. اگر پروفایل کاربر را کامل به‌روز می‌کنید، شیء کامل کاربر را به /users/{id} PUT می‌کنید. برخلاف POST، متد PUT idempotent است. پس فرستادن چندباره همان داده به یک وضعیت نهایی می‌رسد.
  • PATCH: بخشی از منبع را به‌روز می‌کند. وقتی کاربر فقط ایمیلش را عوض می‌کند، فقط فیلد ایمیل را به /users/{id} PATCH می‌کنید. PATCH هم مثل PUT idempotent است.
  • DELETE: منبع را حذف می‌کند. DELETE /bookings/{id} رزرو را لغو می‌کند. این هم idempotent است، چون حذف منبعی که قبلاً حذف شده باید همان نتیجه را بدهد.

نکته کلیدی اینجا idempotency است. عملیاتی که می‌توان بدون تغییر نتیجه با خیال راحت تکرارشان کرد (GET، PUT، PATCH، DELETE) idempotent هستند. وقتی شبکه خراب می‌شود و کلاینت‌ها درخواست را دوباره می‌فرستند، این ویژگی اهمیت پیدا می‌کند. نمی‌خواهید با یک retry رزرو تکراری ساخته شود.

ارسال داده به API

endpointهای API برای این‌که بدانند چه کاری انجام دهند به ورودی نیاز دارند. این ورودی ممکن است منبعی باشد که باید گرفته شود، داده‌ای که باید در پایگاه داده به‌روز شود، یا نحوه فیلتر کردن نتایج. فهمیدن این‌که هر نوع ورودی را کجا بگذارید برای طراحی APIهای تمیز و شهودی خیلی مهم است.

برای ارسال داده به REST API سه گزینه اصلی دارید و هرکدام هدف متفاوتی دارد.

Path parameter مشخص می‌کند با کدام منبع خاص کار می‌کنید. وقتی می‌خواهید جزئیات رویداد را بگیرید، شناسه آن را در مسیر می‌گذارید، مثل /events/123. شناسه بخشی از ساختار URL است و روشن می‌کند یک رویداد مشخص را می‌خواهید، نه مجموعه‌ای از رویدادها. وقتی مقدار برای شناسایی منبع لازم است از path parameter استفاده کنید، چون بدون آن درخواست معنا ندارد.

Query parameter نحوه بازیابی منابع را فیلتر، مرتب یا تغییر می‌دهد. برای جستجوی رویدادهای یک شهر یا بازه تاریخی مشخص از query parameter استفاده می‌کنید، مثل /events?city=NYC&date=2024-01-01. این‌ها اختیاری‌اند. می‌توانید همه رویدادها را بدون فیلتر بخواهید یا چند فیلتر را با هم اعمال کنید. query parameter برای صفحه‌بندی هم خوب است، مثل /events?page=2&limit=20. توجه کنید که اولین پارامتر با ? شروع می‌شود و پارامترهای بعدی با & از هم جدا می‌شوند.

بدنه درخواست (Request Body) داده واقعی‌ای است که برای ساختن یا به‌روزرسانی منابع می‌فرستید. وقتی کاربر بلیت رزرو می‌کند، با جزئیات رزرو (تعداد بلیت، ترجیحات صندلی و غیره) در بدنه به /events/{id}/bookings POST می‌کنید. ساختارهای داده پیچیده و هر چیزی که برای URL زیادی بزرگ یا حساس است در بدنه قرار می‌گیرد.

هر نوع ورودی نقش متفاوتی در قرارداد API دارد. path parameter ساختاری است و مشخص می‌کند به کدام endpoint می‌رسید. query parameter اصلاح‌کننده است و رفتار endpoint را تغییر می‌دهد. بدنه درخواست محموله (payload) است، یعنی داده‌ای که واقعاً با آن کار می‌کنید.

یک مثال عملی. فرض کنید سیستم رزرو می‌سازید و کاربر می‌خواهد بلیت‌های VIP یک رویداد مشخص را رزرو کند. ساختارش این‌طور می‌شود:

1
2
3
4
5
6
7
8
POST /events/123/bookings?notify=true
{
  "tickets": [
    {"section": "VIP", "quantity": 2},
    {"section": "General", "quantity": 1}
  ],
  "payment_method": "credit_card"
}

شناسه رویداد (123) در مسیر است، چون باید مشخص کنید کدام رویداد را رزرو می‌کنید. ترجیح اعلان (notify) query parameter است، چون رفتار اختیاری است. جزئیات واقعی رزرو در بدنه درخواست است، چون داده اصلی‌ای است که ایجاد می‌کنید.

برگرداندن داده

پاسخ API از دو بخش تشکیل می‌شود.

  • کد وضعیت (status code): نشان می‌دهد درخواست موفق بوده یا نه.
  • بدنه پاسخ: حاوی داده‌ای که به کلاینت برمی‌گردانید (معمولاً JSON).

برای کدهای وضعیت به کدهای رایج پایبند باشید: 200 برای موفقیت، 201 برای منبع ساخته‌شده، 400 برای درخواست نامعتبر، 401 برای نیاز به احراز هویت، 404 برای پیدا نشدن و 500 برای خطای سرور.

در مصاحبه زیاد به این فکر نکنید. مصاحبه‌گرها بیشتر به این اهمیت می‌دهند که تفاوت خطای کلاینت (4xx) و خطای سرور (5xx) را بفهمید، نه این‌که تک‌تک کدها را حفظ باشید.

GraphQL

GraphQL در سال ۲۰۱۲ در فیس‌بوک برای حل یک مشکل مشخص به وجود آمد. اپلیکیشن موبایل آن‌ها به داده‌هایی متفاوت از اپلیکیشن وب نیاز داشت، اما با endpointهای REST گیر کرده بودند که ساختار ثابتی برمی‌گرداندند. تیم موبایل مدام endpoint جدید می‌خواست یا تغییر در endpointهای موجود. این کار سرعت توسعه را در هر دو طرف کم می‌کرد.

در REST وقتی کلاینت‌های مختلف به داده‌های متفاوتی نیاز دارند، معمولاً دو انتخاب ناخوشایند دارید. می‌توانید برای کاربردهای مختلف چند endpoint بسازید، که به تکثیر endpoint و سردرد نگهداری می‌انجامد. یا می‌توانید endpointها را طوری بسازید که هر چیزی را که هر کلاینتی ممکن است لازم داشته باشد برگردانند، که به over-fetching می‌رسد و کلاینت‌های موبایل مگابایت‌ها داده بی‌استفاده دانلود می‌کنند.

GraphQL endpointهای منبع را در یک endpoint واحد ادغام می‌کند که کوئری‌هایی می‌پذیرد که دقیقاً توصیف می‌کنند چه داده‌ای می‌خواهید. کلاینت شکل پاسخ را مشخص می‌کند و سرور داده را دقیقاً با همان قالب برمی‌گرداند.

GraphQL چطور کار می‌کند

یک مثال ساده با سناریوی Ticketmaster. به‌جای endpointهای جدا برای رویدادها، سالن‌ها و بلیت‌ها، یک endpoint GraphQL دارید که می‌تواند کوئری‌هایی مثل این را مدیریت کند:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
query {
  event(id: "123") {
    name
    date
    venue {
      name
      address
    }
    tickets {
      section
      price
      available
    }
  }
}

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

زمان استفاده از GraphQL در مصاحبه

GraphQL وقتی انتخاب خوبی است که کلاینت‌های متنوعی با نیازهای داده‌ای متفاوت دارید. اگر مصاحبه‌گر سناریوهایی مثل «اپلیکیشن موبایل داده‌های متفاوتی از اپلیکیشن وب لازم دارد» را مطرح کند، یا بپرسد «چطور از over-fetching و under-fetching جلوگیری می‌کنید»، احتمالاً می‌خواهد شما GraphQL را مطرح کنید.

همچنین وقتی تیم‌های فرانت‌اند باید بدون تغییر بک‌اند سریع تکرار کنند انتخاب خوبی است. در REST، اضافه کردن یک فیلد جدید به صفحه موبایل اغلب به تغییر بک‌اند و deploy کردن API نیاز دارد. در GraphQL تیم فرانت‌اند می‌تواند تا وقتی فیلدها در schema موجودند، هر چه خواست درخواست کند.

با این حال GraphQL پیچیدگی اضافه می‌کند. باید query parsing، اعتبارسنجی schema و اغلب استراتژی‌های caching پیچیده را پیاده‌سازی کنید. برای بیشتر مصاحبه‌های طراحی سیستم، REST ساده‌تر و مستقیم‌تر است، مگر این‌که مسئله صراحتاً به انعطاف GraphQL نیاز داشته باشد.

طراحی Schema در GraphQL

اگر GraphQL را انتخاب کنید باید طراحی را متفاوت ببینید. به‌جای endpointهای منبع در REST، schemaای طراحی می‌کنید که انواع داده و روابط بین آن‌ها را تعریف می‌کند.

در مثال Ticketmaster، با مدل کردن موجودیت‌های اصلی به‌صورت type در GraphQL شروع می‌کنید:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Event {
  id: ID!
  name: String!
  date: DateTime!
  venue: Venue!
  tickets: [Ticket!]!
}

type Venue {
  id: ID!
  name: String!
  address: String!
}

type Query {
  event(id: ID!): Event
  events(limit: Int, after: String): [Event!]!
}

تفاوت اصلی با REST این است که روابط را مستقیم در schema تعریف می‌کنید. یک رویداد یک سالن دارد و کلاینت‌ها می‌توانند در یک کوئری از این رابطه عبور کنند.

اما همین انعطاف مشکل N+1 را می‌آورد، که بزرگ‌ترین نقطه ضعف GraphQL است. وقتی کلاینت رویدادها را همراه سالن‌هایشان کوئری می‌کند، ممکن است یک کوئری برای رویدادها اجرا کنید و بعد برای هر سالن N کوئری جدا. با ۱۰۰ رویداد، به‌جای ۲ کوئری، ۱۰۱ کوئری پایگاه داده خواهید داشت. راه‌حل، الگوهای batching یا data loader است که کوئری‌های مرتبط را گروه‌بندی می‌کند. اما پیچیدگی‌هایی اضافه می‌کند که در REST ندارید.

GraphQL مجوزدهی را هم متفاوت مدیریت می‌کند. به‌جای ایمن کردن کل endpointها مثل REST، فیلدهای تک‌تک را ایمن می‌کنید. ممکن است کاربری نام و تاریخ رویداد را ببیند اما داده‌های درآمد را نه. این را در سطح فیلد و در resolverهای schema کنترل می‌کنید.

در مصاحبه GraphQL را وقتی مطرح کنید که مشکل واضح over-fetching یا under-fetching می‌بینید، اما آن را پیش‌فرض نکنید. بیشتر مصاحبه‌گرها از دانستن GraphQL استقبال می‌کنند، ولی معمولاً ترجیح می‌دهند اول چالش‌های اصلی معماری را با ابزارهای ساده‌تر حل کنید.

RPC

RPC (فراخوانی رویه از راه دور) پروتکلی است که به کلاینت اجازه می‌دهد رویه‌ای را روی سرور فراخوانی کند و منتظر پاسخ بماند، بدون این‌که لازم باشد جزئیات شبکه را بفهمد. برای ارتباط بین سرویس‌ها از HTTP سریع‌تر است، به‌خصوص وقتی کارایی بالا و تأخیر کم لازم دارید.

RPC چطور کار می‌کند

برخلاف رویکرد منبع‌محور REST، RPC عمل‌محور است. عملاً توابع را روی شبکه فراخوانی می‌کنید، انگار توابع محلی در کدبیس‌تان هستند. همان Ticketmaster با RPC این‌طور می‌شود:

1
2
3
4
5
6
7
8
// Instead of GET /events/123
getEvent(eventId: "123")

// Instead of POST /events/123/bookings
createBooking(eventId: "123", userId: "456", tickets: [...])

// Instead of GET /events/123/tickets
getAvailableTickets(eventId: "123", section: "VIP")

محبوب‌ترین پروتکل RPC امروز gRPC است که برای serialization از Protocol Buffers و برای انتقال از HTTP/2 استفاده می‌کند. این ترکیب خیلی سریع‌تر از رویکرد JSON روی HTTP در REST است، به‌خصوص برای ارتباط سرویس‌به‌سرویس. چارچوب RPC قابل‌توجه دیگر Apache Thrift است که ابتدا در فیس‌بوک توسعه یافت و حالا متن‌باز است و چند زبان برنامه‌نویسی و قالب serialization را پشتیبانی می‌کند.

Protocol Buffers و ایمنی نوع

gRPC برای تعریف قرارداد سرویس‌ها از Protocol Buffers (protobuf) استفاده می‌کند. یک فایل .proto می‌نویسید که متدهای سرویس و ساختار داده را توصیف می‌کند:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
service TicketService {
  rpc GetEvent(GetEventRequest) returns (Event);
  rpc CreateBooking(CreateBookingRequest) returns (Booking);
  rpc GetAvailableTickets(GetTicketsRequest) returns (TicketList);
}

message GetEventRequest {
  string event_id = 1;
}

message Event {
  string id = 1;
  string name = 2;
  int64 date = 3;
  Venue venue = 4;
}

gRPC از همین تعریف واحد کد کلاینت و سرور را برای چند زبان برنامه‌نویسی تولید می‌کند. یعنی سرویس بک‌اند Go و سرویس پرداخت Java شما می‌توانند با ایمنی نوع (type safety) در زمان کامپایل با هم ارتباط بگیرند و ناسازگاری‌ها قبل از deploy پیدا شوند.

زمان استفاده از RPC در مصاحبه

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

  • کارایی بسیار مهم است: serialization باینری و HTTP/2 باعث می‌شود RPC به‌طور قابل‌توجهی از JSON REST سریع‌تر باشد.
  • ایمنی نوع مهم است: کد کلاینت تولیدشده جلوی خیلی از خطاهای زمان اجرا را می‌گیرد.
  • ارتباط سرویس‌به‌سرویس: APIهای داخلی بین سرویس‌های شما به معنای منابع REST نیاز ندارند.
  • streaming لازم است: gRPC از streaming دوطرفه برای قابلیت‌های بلادرنگ پشتیبانی می‌کند.

برای مثال Ticketmaster می‌توانید برای endpointهای عمومی که اپلیکیشن‌های موبایل و وب استفاده می‌کنند، REST بگذارید. اما برای ارتباط داخلی بین سرویس رزرو، سرویس پرداخت و سرویس موجودی از gRPC استفاده کنید.

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

الگوهای مشترک API

چه REST، چه GraphQL و چه RPC انتخاب کنید، بعضی الگوها در همه انواع API کاربرد دارند. شناختنشان ارزش دارد، چون در بیشتر سیستم‌های واقعی هستند.

صفحه‌بندی (Pagination)

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

به‌جای آن به صفحه‌بندی نیاز دارید تا مجموعه نتایج بزرگ را به تکه‌های قابل‌مدیریت تقسیم کنید. دو رویکرد اصلی وجود دارد، مبتنی بر offset و مبتنی بر cursor.

صفحه‌بندی مبتنی بر Offset: ساده‌ترین روش است و بیشتر وب‌سایت‌ها از آن استفاده می‌کنند. مشخص می‌کنید چند رکورد رد شود و چند رکورد برگردد. مثلاً /events?offset=20&limit=10 رکوردهای ۲۱ تا ۳۰ را می‌گیرد. این روش شهودی و پیاده‌سازی‌اش آسان است، اما با مجموعه داده بزرگ مشکل دارد. اگر هنگام صفحه‌بندی کسی رویداد جدیدی اضافه کند، داده‌ها جابه‌جا می‌شوند و ممکن است رکورد تکراری ببینید یا رکوردی را از دست بدهید.

صفحه‌بندی مبتنی بر Cursor: این روش با استفاده از یک اشاره‌گر به رکورد مشخص، به‌جای شمردن از ابتدا، این مشکل را حل می‌کند. در عمل این‌طور کار می‌کند. در درخواست اول /events?limit=10 را می‌فرستید و پاسخ شامل رویدادها و cursor است که به آخرین رکورد اشاره می‌کند:

1
2
3
4
{
  "events": [...],
  "next_cursor": "cmd9atj3p000007ky19w1dpy2"
}

درخواست بعدی /events?cursor=cmd9atj3p000007ky19w1dpy2&limit=10 است.

cursor معمولاً ارجاع رمزگذاری‌شده به یک رکورد مشخص است، مثل شناسه یا timestamp. این روش پایدارتر است، چون رکوردهای جدید روی آن اثر نمی‌گذارند. اما پیاده‌سازی قابلیت‌هایی مثل «پرش به صفحه ۵» دشوارتر است. در مثال بالا، cmd9atj3p000007ky19w1dpy2 شناسه آخرین رویداد صفحه اول است.

در مصاحبه، صفحه‌بندی مبتنی بر offset معمولاً کافی است، مگر با داده بلادرنگ سروکار داشته باشید یا مصاحبه‌گر مشخصاً درباره سناریوهای پرحجم بپرسد. بیشتر مصاحبه‌گرها بیشتر به این اهمیت می‌دهند که صفحه‌بندی را فراموش نکرده باشید، تا این‌که کدام رویکرد را انتخاب می‌کنید.

استراتژی‌های نسخه‌سازی

APIها در طول زمان تکامل پیدا می‌کنند و برای مدیریت تغییرات بدون خراب کردن کلاینت‌های موجود به استراتژی نیاز دارید. این برای APIهای عمومی، که کنترل نمی‌کنید کلاینت‌ها کی کدشان را به‌روز می‌کنند، مخصوصاً مهم است.

رایج‌ترین رویکرد نسخه‌سازی در URL است، یعنی شماره نسخه را در مسیر می‌گذارید، مثل /v1/events یا /v2/events. این روش روشن و قابل‌فهم است. کلاینت‌ها فقط با دیدن URL دقیقاً می‌دانند از کدام نسخه استفاده می‌کنند. پیاده‌سازی‌اش هم ساده است، چون می‌توانید نسخه‌های مختلف را به مسیرهای کد مختلف هدایت کنید.

نسخه‌سازی در هدر شماره نسخه را به‌جای آن در هدر HTTP می‌گذارد، مثل Accept-Version: v2 یا API-Version: 2. این کار URLها را تمیزتر نگه می‌دارد و بیشتر با استانداردهای HTTP هماهنگ است، اما برای توسعه‌دهندگان کمتر مشخص است و تست آن در مرورگر سخت‌تر است.

برای مصاحبه نسخه‌سازی در URL معمولاً انتخاب مطمئن‌تری است، چون گسترده‌تر شناخته شده و توضیحش سریع‌تر است. مگر این‌که مصاحبه‌گر مشخصاً درباره نسخه‌سازی در هدر بپرسد، از رویکرد URL استفاده کنید.

خواهید دید که ما در تحلیل مسائل حتی نسخه‌سازی را در طراحی API نمی‌آوریم. دلیلش بیشتر این است که برای بیشتر مصاحبه‌گرها مهم نیست، نه این‌که در عمل مهم نباشد (مهم است).

ملاحظات امنیتی

امنیت اغلب در مصاحبه‌ها موضوعی دیرهنگام به حساب می‌آید، اما نشان دادن آگاهی امنیتی می‌تواند شما را متمایز کند. لازم نیست سیستم امنیتی نفوذناپذیر طراحی کنید، اما نشان دادن این‌که اصول پایه امنیت API را می‌دانید، نشان می‌دهد به سیستم‌های آماده production فکر می‌کنید.

احراز هویت و مجوزدهی

اولین سوالی که API شما باید جواب بدهد این است: «چه کسی این درخواست را می‌دهد و آیا اجازه دارد کاری را که می‌خواهد انجام دهد؟»

احراز هویت (Authentication) هویت را تأیید می‌کند، یعنی ثابت می‌کند کاربر همان کسی است که ادعا می‌کند. مجوزدهی (Authorization) دسترسی‌ها را بررسی می‌کند، یعنی می‌سنجد آیا کاربر احراز هویت‌شده اجازه انجام اقدام خاصی را که درخواست کرده دارد یا نه.

در مثال Ticketmaster، احراز هویت ممکن است تأیید کند درخواست از طرف کاربر [email protected] می‌آید، و مجوزدهی بررسی کند آیا جان اجازه دارد رزرو مشخصی را لغو کند. او فقط باید بتواند رزروهای خودش را لغو کند، نه همه رزروها را.

کلید API در برابر توکن JWT

هنگام طراحی احراز هویت API، معمولاً بین دو رویکرد اصلی انتخاب می‌کنید که به این بستگی دارد چه کسی از API استفاده می‌کند و چطور به آن دسترسی دارد.

برای بیشتر مصاحبه‌ها (نه همه)، احراز هویت و مجوزدهی تمرکز اصلی نیست. توصیه من این است که endpointهایی را که کاربر باید احراز هویت شود مشخص کنید و بگویید برای احراز هویت کاربر به JWT تکیه می‌کنید یا نشست کاربر را در پایگاه داده ذخیره می‌کنید.

کلید API: کلیدهای API رشته‌های طولانی و تصادفی هستند که مثل رمز عبور برای برنامه‌ها عمل می‌کنند، نه برای انسان‌ها. وقتی کلاینت درخواست می‌دهد، کلید API خود را در هدر Authorization می‌گذارد و سرور شما آن کلید را جستجو می‌کند تا بفهمد کدام برنامه درخواست را فرستاده.

طرز کارشان این است که برای هر کلاینت یک کلید API یکتا تولید می‌کنید (مثل sk_live_abc123def456...)، آن را همراه مجوزها یا محدودیت نرخ آن کلاینت در پایگاه داده ذخیره می‌کنید و بعد هر درخواست ورودی را با جستجوی کلید اعتبارسنجی می‌کنید. این روش برای ارتباط سرور به سرور، که هر دو طرف را کنترل می‌کنید، عالی است. وقتی سرویس رزرو باید با سرویس پرداخت ارتباط بگیرد، کلید API ساده و مؤثر است. همچنین وقتی endpointهایتان را در اختیار توسعه‌دهندگان شخص ثالث می‌گذارید که به دسترسی برنامه‌نویسی به سیستم نیاز دارند، منطقی است.

اگر محصول رو به کاربر با APIهای رو به کاربر می‌سازید، کلید API تقریباً هیچ‌وقت انتخاب درستی نیست. کاربران نباید رشته‌های طولانی رمزنگاری را مدیریت کنند. کلیدهای API هم مثل نشست کاربر منقضی نمی‌شوند و context کاربر را با خود ندارند.

1
2
GET /events
Authorization: Bearer sk_live_abc123...

JWT (JSON Web Token): توکن‌های JWT برعکس، اطلاعات کاربر را مستقیم داخل خود توکن رمزگذاری می‌کنند و لازم نیست وضعیت نشست روی سرور ذخیره شود. وقتی کاربر با موفقیت وارد می‌شود، سرور شما یک JWT می‌سازد که شناسه کاربر، مجوزها و زمان انقضا را دارد و کل توکن را با یک کلید مخفی امضا می‌کند.

جالب این‌که وقتی این JWT با درخواست‌های بعدی برمی‌گردد، می‌توانید با بررسی امضا اصالتش را تأیید کنید و اطلاعات کاربر را مستقیم از توکن و بدون جستجو در پایگاه داده بخوانید. خود توکن همه context لازم برای اعتبارسنجی درخواست را دارد.

JWTها برای سیستم‌های توزیع‌شده به‌خصوص خوب‌اند، چون هر سرویسی که کلید امضا را بداند می‌تواند مستقل توکن را تأیید کند. اگر اپلیکیشن موبایل شما یک JWT به API Gateway بفرستد، gateway می‌تواند هویت کاربر را تأیید کند و با اطمینان درخواست را به سرویس رزرو بفرستد.

1
2
3
4
5
6
{
  "user_id": "123",
  "email": "[email protected]",
  "role": "customer",
  "exp": 1640995200
}

از کلید API برای ارتباط داخلی سرویس‌ها و دسترسی توسعه‌دهندگان خارجی استفاده کنید. از JWT برای نشست کاربر در اپلیکیشن‌های وب و موبایل استفاده کنید. JWTها می‌توانند stateless باشند (بدون نیاز به جستجو در پایگاه داده) و context کاربر را حمل کنند. به همین دلیل برای اپلیکیشن‌های رو به کاربر ایده‌آل‌اند.

کنترل دسترسی مبتنی بر نقش (RBAC)

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

RBAC نقش‌ها را به کاربران و مجوزها را به نقش‌ها اختصاص می‌دهد:

1
2
3
4
5
6
7
Roles:
- customer: can book tickets, view own bookings
- venue_manager: can create events, view sales for their venues
- admin: can access everything

User: [email protected] -> Role: customer
User: [email protected] -> Role: venue_manager

در طراحی API هم احراز هویت و هم مجوزدهی را بررسی می‌کنید:

1
2
3
GET /bookings/{id}
1. Is the user authenticated? (valid JWT token)
2. Is the user authorized? (owns this booking OR is admin)

حداکثر در مصاحبه فقط می‌گویید کدام endpointها را کدام نقش‌ها می‌توانند بزنند، هرچند در بیشتر موارد این تمایز مرتبط نیست.

Rate Limiting و Throttling

Rate Limiting با محدود کردن تعداد درخواست‌هایی که کلاینت در یک بازه زمانی می‌تواند بدهد، جلوی سوءاستفاده را می‌گیرد. این کار سیستم شما را از حمله‌های مخرب و استفاده بیش از حد تصادفی محافظت می‌کند.

استراتژی‌های رایج:

  • محدودیت برای هر کاربر: مثلاً ۱۰۰۰ درخواست در ساعت برای هر کاربر احراز هویت‌شده.
  • محدودیت برای هر IP: مثلاً ۱۰۰ درخواست در ساعت برای درخواست‌های بدون احراز هویت.
  • محدودیت برای هر endpoint: مثلاً ۱۰ رزرو در دقیقه برای جلوگیری از احتکار بلیت.

معمولاً Rate Limiting را در سطح API Gateway یا با middleware در برنامه پیاده‌سازی می‌کنید. وقتی کلاینت از محدودیت‌ها رد شد، کد وضعیت 429 Too Many Requests را برگردانید.

در مصاحبه، اشاره به Rate Limiting نشان می‌دهد نگرانی‌های production را می‌فهمید. اما مگر این‌که از شما بخواهند، برای طراحی الگوریتم مشخص وقت نگذارید. یک جمله ساده مثل «Rate Limiting برای جلوگیری از سوءاستفاده» معمولاً کافی است.

جمع‌بندی

طراحی API در مصاحبه‌های طراحی سیستم درباره نشان دادن قضاوت مهندسی محکم است، نه نوشتن مشخصات کامل. روی انتخاب پروتکل مناسب (معمولاً REST)، مدل کردن واضح منابع و نشان دادن درک اصول پایه احراز هویت و امنیت تمرکز کنید.

در مصاحبه همه‌چیز درباره تعادل است. به‌اندازه کافی وقت بگذارید تا نشان دهید می‌توانید API معقولی طراحی کنید، اما وقتی چالش‌های بزرگ‌تر معماری وجود دارد، درگیر جزئیات نشوید. مصاحبه‌گر می‌خواهد ببیند می‌توانید سیستم‌هایی بسازید که کار می‌کنند، نه این‌که همه کدهای وضعیت HTTP را حفظ باشید.

در عمل، کاندیداها بیشتر به‌خاطر صرف زمان زیاد برای طراحی API دچار مشکل می‌شوند تا به‌خاطر سرمایه‌گذاری ناکافی. تمام تلاشتان را بکنید که بیش از ۵ دقیقه برای شرح APIها در مصاحبه صرف نکنید.