Pagination، Filtering و Sorting در API چگونه باید طراحی شوند؟

صفحه سفارشهای پنل مدیریت یک فروشگاه آنلاین را در نظر بگیرید؛ همان صفحهای که تیم عملیات هر روز صبح باز میکند تا سفارشهای شب گذشته را مرور کند. هفتههای نخست پس از عرضه سرویس، همهچیز روان است. اما با هر هفته که به دادههای فروش اضافه میشود، باز شدن همین یک صفحه کندتر میشود، تا جایی که گوشی برخی مدیران هنگام لود آن بهسختی نفس میکشد.
ریشه این کندی معمولاً در رابط کاربری نیست؛ در قراردادی است که میان سرویسها بسته شده است: یک نقطه اتصال (Endpoint) که فهرست سفارشها را ارائه میدهد و تصمیم گرفته هر آنچه در پایگاه داده هست را یکجا برگرداند. اینجا دقیقاً جایی است که سه ابزار طراحی API به کمک میآیند: صفحهبندی (Pagination)، پالایش (Filtering) و مرتبسازی (Sorting).
مسئله: پاسخی که میخواهد همهچیز را بفرستد
سرویسی که فهرست را بدون محدودیت برمیگرداند، در روز نخست شاید فقط چند صد سطر داشته باشد و مشکلی دیده نشود. اما دادههای تراکنشی مثل سفارش، بلیت، پیام یا گزارش، ذاتاً رو به رشدند. هر ماه حجم پاسخ بیشتر میشود، تا روزی که شبکه، حافظه سرور یا مرورگر کاربر باید به همان بزرگی پاسخ بدهد که کل کسبوکار تا آن روز ساخته است.
این الگو سه پیامد زنجیرهای دارد. نخست، زمان پاسخ بالا میرود و تجربه کاربری میسوزد. دوم، درخواستهای سنگین منابع سرور را اشغال میکنند و گاهی کل سرویس را برای بقیه کاربران کند میکنند. سوم، کلاینتهایی که روی موبایل یا شبکه ضعیف کار میکنند دیرتر پاسخ را میگیرند یا هرگز نمیگیرند؛ درخواست با خطای انقضا (Timeout) شکست میخورد و کاربر گمان میکند سیستم خراب است.
پیامد پنهانتر، مالی است: هر درخواست بیسقف هزینه ترافیک و پردازش دارد و اگر کلاینتهای متعدد همین رفتار را تکرار کنند، ظرفیت زیرساخت صرف ارسال دادهای میشود که بیشترش هرگز دیده نمیشود. یعنی مشکل فقط «کندی» نیست؛ اتلاف سیستماتیک منابع است.
راهکار: بستن قرارداد روشن برای دریافت داده
راهکار، بازطراحی قرارداد سرویس است تا کلاینت دقیقاً همان چیزی را بگیرد که لازم دارد، در ترتیبی که میخواهد و به اندازهای که میتواند نمایش دهد. این قرارداد سه ستون دارد که در ادامه یکییکی بررسی میشوند.
ستون نخست: صفحهبندی
صفحهبندی یعنی پاسخ بهجای کل مجموعه، یک برش از آن را برگرداند. رایجترین سبک، صفحهبندی شمارهای است: کلاینت شماره صفحه و اندازه هر صفحه را میفرستد و سرویس همان برش را میدهد. این سبک ساده و قابل فهم است و برای پنلهای مدیریتی که کاربر میخواهد میان صفحات جابهجا شود، انتخاب طبیعی است.
اما یک ضعف پنهان دارد: اگر میان دو درخواست، سطری اضافه یا حذف شود، مرز صفحات جابهجا میشود و کاربر ممکن است یک سطر را دوباره ببیند یا یکی را هرگز نبیند. برای فیدها و جریانهای زنده، سبک دوم یعنی صفحهبندی مکاننمایی (Cursor Pagination) بهتر جواب میدهد: پاسخ همراه دادهها یک نشانه ادامه هم میفرستد و درخواست بعدی از همان نقطه ادامه مییابد؛ بدون آنکه تغییر دادههای تازه، ترتیب دیدن را به هم بزند.
ستون دوم: پالایش
صفحهبندی به کاربر صفحه میدهد، اما اگر هزار صفحه وجود داشته باشد، ورق زدن یعنی شکست طراحی. پالایش به کلاینت اجازه میدهد مجموعه را پیش از برش کوچک کند: فقط سفارشهای در انتظار پرداخت، فقط بازه زمانی مشخص، فقط شهر خاص. هر پارامتر پالایش باید نام روشن، مقادیر مجاز مشخص و رفتار مستندشده داشته باشد؛ در غیر این صورت هر مصرفکننده سرویس، برداشت خودش را از آن میسازد.
یک نکته کمتر دیدهشده: پالایش باید در سمت داده انجام شود، نه بعد از دریافت. اگر سرویس همه رکوردها را بخواند و سپس در حافظه پالایش کند، عملاً مشکل اولیه فقط جابهجا شده است. محدودیتها باید تا لایه ذخیرهسازی پیش بروند تا مزیت واقعیشان آشکار شود.
ستون سوم: مرتبسازی
بدون مرتبسازی صریح، ترتیب پاسخ به تصمیمهای داخلی پایگاه داده بستگی دارد و ممکن است میان دو درخواست یکسان فرق کند. کاربری که صفحه پنجم را دیده و به صفحه چهارم برمیگردد، نباید سطر تکراری یا گمشده ببیند. قرارداد سرویس باید مرتبسازی پیشفرض پایداری داشته باشد، مثلاً جدیدترینها اول، و برای ثبات کامل، در صورت برابری مقادیر اصلی، معیار دومی مثل شناسه رکورد به کار بگیرد.
همچنین فیلدهایی که با آنها میتوان مرتب کرد باید محدود و اعلامشده باشند. مرتبسازی آزاد روی هر فیلد دلخواه، هم هزینه محاسباتی پرهزینهای دارد و هم معمولاً به ساختارهای پشتیبان در پایگاه داده نیاز دارد که وجودشان تضمین نشده است.
مثال کاربردی: قرارداد سرویس سفارشهای فروشگاه
فرض کنید تیم فروشگاه آنلاین میخواهد نقطه اتصال فهرست سفارشها را بازطراحی کند. بهجای پاسخ بدون مرز، این قرارداد را میبندد: کلاینت پارامترهایی میفرستد که هرکدام نقش مشخصی دارند. جدول زیر این قرارداد را خلاصه میکند.
| پارامتر | نقش | منطق پیشنهادی طراحی |
|---|---|---|
| page | شماره صفحه درخواستی | از یک شروع میشود و در صورت ارسالنشدن، مقدار پیشفرض در نظر گرفته میشود. |
| per_page | تعداد آیتم هر صفحه | پیشفرض معقول تعیین و سقف سختی برای آن گذاشته میشود تا درخواستهای افراطی پاسخ سنگین نسازند. |
| status | پالایش بر اساس وضعیت سفارش | فقط مقادیر مجازِ منتشرشده در مستندات پذیرفته میشود؛ مقدار بیرون از فهرست با پیام روشن رد میشود. |
| created_from | ابتدای بازه زمانی ثبت سفارش | برای گزارشهای دورهای؛ دو سر بازه جداگانه تعریف میشود تا ترکیبها منعطف بماند. |
| created_to | انتهای بازه زمانی ثبت سفارش | در کنار پارامتر قبلی معنا مییابد و حالت تکسری هم باید رفتار مستند داشته باشد. |
| sort | فیلد مرتبسازی | از میان فیلدهای مجاز و پشتیبانیشده انتخاب میشود، نه هر فیلد دلخواه. |
| order | جهت مرتبسازی | صعودی یا نزولی؛ مقدار نامعتبر بهجای رفتار مبهم، با پیام صریح رد میشود. |
با این قرارداد، درخواستی که «صفحه دوم، دهتایی، فقط در انتظار پرداخت، مرتب بر اساس جدیدترین» را میخواهد، پاسخی سبک و پیشبینیپذیر میگیرد. سرویس هم میتواند برای پارامترهای پالایش و مرتبسازی، ساختارهای پشتیبان در پایگاه داده تعریف کند، چون مجموعه مقادیر مجاز محدود و معلوم است.
یک تکمله مهم: پاسخ فهرستی بهتر است متادیتای صفحه را هم برگرداند؛ شماره صفحه جاری، اندازه صفحه و کل تعداد رکوردهای منطبق. این اطلاعات کمهزینه است اما کلاینت را از حدس زدن نجات میدهد و اجازه میدهد رابط کاربری، ناوبری درستی بسازد.
مزایا، محدودیتها و خطاهای رایج
مزیت سه ستون بالا روشن است: پاسخ کوچکتر و سریعتر، فشار کمتر روی سرور و شبکه، و تجربهای که با رشد داده خراب نمیشود. محدودیت واقعی در هزینه طراحی است: باید مقادیر پیشفرض تعیین شود، مستندات نوشته شود و رفتار پارامترهای نامعتبر تعریف شود. در مقابل، خطاهای رایجی که همین طراحی را خراب میکنند عبارتاند از:
- بیسقف گذاشتن اندازه صفحه؛ درخواستی که میخواهد همهچیز را در یک صفحه بگیرد، همان پاسخ سنگین اولیه را بازسازی میکند.
- پالایش و مرتبسازی روی هر فیلد دلخواه؛ هزینه محاسباتی نامحدود و رفتار غیرقابل پیشبینی.
- استفاده از صفحهبندی شمارهای برای جریانهای زنده بدون توجه به جابهجایی مرزها؛ نتیجهاش سطرهای تکراری یا گمشده است.
- پاسخ خالی بیمعنا؛ وقتی پالایش نتیجهای ندارد، کلاینت باید بداند «خالی است چون چیزی نبود»، نه اینکه به خرابی مشکوک شود.
- نادیده گرفتن بیسروصدای پارامترهای ناشناخته؛ کاربر گمان میکند پالایش اعمال شده، در حالی که سرویس آن را کنار گذاشته است.
کاربرد عملی: از قرارداد تا نگهداری
در عمل، طراحی خوب از سه قدم عبور میکند. قدم نخست، شناخت الگوی مصرف است: کدام فیلترها واقعاً استفاده میشوند، کدام مرتبسازی مدنظر کاربران است و چه اندازه صفحهای برای کلاینتهای موجود معقول است. قدم دوم، مستندسازی صریح است: نام پارامترها، مقادیر مجاز، پیشفرضها و سقفها همه باید در مستندات زندگی کنند، نه در حافظه توسعهدهندهها. قدم سوم، پایش پس از انتشار است: توزیع اندازه صفحات درخواستی، فراوانی هر فیلتر و تعداد خطاها نشان میدهد قرارداد با واقعیت مصرف همخوان است یا باید اصلاح شود.
نکته پایانی برای تیمها: صفحهبندی و پالایش، کار را به لایه ذخیرهسازی هم میسپارند؛ ستونهایی که روی آنها پالایش و مرتبسازی میشود، از ساختارهای پشتیبان مثل شاخصها بهره میبرند و بدون آن پشتیبانی، قرارداد زیبای سرویس در عمل کند میماند.
نکات کلیدی این مقاله
- هیچ نقطه اتصال فهرستی نباید بدون سقف پاسخ بدهد؛ اندازه صفحه پیشفرض و سقف سختی، بخشی از قرارداد است.
- برای پنلهای مدیریتی صفحهبندی شمارهای و برای جریانهای زنده صفحهبندی مکاننمایی انتخاب بهتری است.
- پالایش و مرتبسازی فقط روی فیلدهای مجاز و مستندشده انجام شود تا هزینه محاسباتی مهار بماند.
- پاسخ فهرستی بدون متادیتای صفحه ناقص است؛ کلاینت باید موقعیت خود را بداند، نه حدس بزند.
- پارامتر ناشناخته یا نامعتبر نباید بیسروصدا نادیده گرفته شود؛ رد شدن با پیام روشن، قرارداد را معتبر نگه میدارد.
سوالات متداول
بهترین اندازه صفحه برای پاسخ API چند آیتم است؟
عدد جادویی جهانی وجود ندارد؛ اندازه مناسب به وزن هر رکورد و نوع کلاینت بستگی دارد. اصل مهم این است که پیشفرضی معقول انتخاب شود، سقفی سختی برای اندازه درخواستی گذاشته شود و رفتار سرویس با پایش واقعی مصرف بازبینی شود، نه اینکه بر اساس حدس اولیه قفل بماند.
صفحهبندی مکاننمایی جایگزین کامل صفحهبندی شمارهای است؟
نه. مکاننما برای جریانهای زنده و حرکت به جلو عالی است، اما پرش آزاد میان صفحات دلخواه را گران میکند. پنلهایی که کاربرشان صفحات را ورق میزند با شماره صفحه راحتتر هستند؛ پس انتخاب به الگوی مصرف واقعی کلاینتها بستگی دارد.
اگر پالایش در سمت کلاینت انجام شود چه اشکالی دارد؟
کلاینت باید همه دادهها را بگیرد تا بتواند پالایش کند؛ یعنی همان پاسخ سنگینی که از ابتدا از آن فرار میکردیم. پالایش درست باید تا لایه ذخیرهسازی برسد تا فقط داده منطبق منتقل شود. پالایش سمت کلاینت فقط برای مجموعههای کوچک و ثابت قابل قبول است.



