طراحی URI در API؛ چرا آدرسهای اشتباه پروژه را میشکنند؟
طراحی URI در API از نامگذاری منابع تا نسخهبندی و امنیت؛ چارچوبی عملی برای ساخت endpointهایی که سالها پایدار میمانند.
طراحی URI ظ(Uniform Resource Identifier) در API (Application Programming Interface) یکی از بنیادیترین تصمیمهای معماری است که کیفیت آن تا سالهای طولانی بر تجربه توسعهدهندگان و پایداری سیستم اثر میگذارد. یک URI خوب مانند یک قرارداد پایدار عمل میکند و کلاینتها میتوانند با اطمینان روی آن حساب باز کنند. در مقابل، URI ضعیف باعث میشود هر بازآرایی کوچک در ساختار داده، مستندات را از اعتبار بیندازد و نسخههای کلاینت را یکییکی بشکند. این نوشتار از تفاوت URI و URL و URN شروع میشود و تا اصول نامگذاری منابع، ساختار سلسلهمراتبی، نسخهبندی، صفحهبندی، امنیت و دامهای پنهان پیش میرود. تمرکز اصلی بر آن است که چطور میتوان URIهایی طراحی کرد که در مقیاس هزاران endpoint هم قابل نگهداری بمانند و سالها بدون شکستن کلاینتها زنده بمانند.
در پروژههای متعددی که با تیمهای مختلف همراه بودهام، یک الگوی مشخص بارها دیده شده است: تیم بکاند با عجله نخستین نسخه API را تحویل میدهد، URIها بر پایه نیاز لحظهای ساخته میشوند، و چند ماه بعد که تیم موبایل و وب به نسخه دوم میرسند، مشخص میشود نیمی از آدرسها قابل تغییر نیستند. این متن نتیجه بازبینی همان تجربههاست.
URI و URL و URN؛ مرزها کجاست؟
پیش از هر بحثی درباره طراحی، باید مرز دقیق مفاهیم را روشن کرد چون در گفتگوهای روزمره تیمها این سه اصطلاح بهجای هم بهکار میروند و همین ابهام، اولین منبع تصمیمهای اشتباه است. بر اساس RFC 3986، یک URI هر رشتهای است که یک منبع را بهصورت یکتا شناسایی کند؛ خواه آن منبع یک صفحه وب باشد، خواه یک فایل روی دیسک، خواه یک مفهوم انتزاعی مثل یک شماره تلفن.
URI در واقع یک ابرمفهوم است و دو زیرمجموعه دارد: URL (Uniform Resource Locator) که علاوه بر شناسایی، محل دسترسی منبع را هم مشخص میکند، و URN (Uniform Resource Name) که فقط شناسهای پایدار ارائه میدهد بدون آنکه به مکان فیزیکی اشاره کند. برای درک عمیقتر تفاوت URI و URL و کاربردهای هرکدام، مراجعه به راهنمای تفاوت URI و URL نقطه شروع مناسبی است.
یک مثال ساده تفاوت را روشن میکند:
https://api.example.com/v1/users/42/orders?status=shipped#summary
└─┬──┘ └──────┬───────┘└┬┘└────┬────┘└─────┬─────┘└──┬───┘└──┬──┘
scheme authority path path query fragment
└────────────────────── URL ──────────────────────┘
└────────────────────────── URI ──────────────────────────────┘
در همین ساختار، URN فقط بخش شناسه را نگه میدارد؛ مثلاً urn:isbn:978-3-16-148410-0 یک کتاب را معرفی میکند بدون آنکه بگوید کجا ذخیره شده است. اگر با مفاهیم URN و کاربردهای آن در وب آشنایی ندارید، راهنمای URN در منابع دیجیتال این لایه را باز میکند.
| ویژگی | URI | URL | URN |
|---|---|---|---|
| نقش اصلی | شناسایی یکتا | شناسایی + مکان | شناسایی پایدار |
| وابستگی به مکان | ممکن است داشته باشد | دارد | ندارد |
| پایداری در مهاجرت | متوسط | پایین | بالا |
URI یک قرارداد است، نه یک آدرس ساده؛ تفاوت این دو نگاه، تفاوت بین APIای است که یک دهه دوام میآورد و APIای که هر سال بازنویسی میشود.
در سطح فنی، تفکیک این سه مفهوم فقط یک تمرین دانشگاهی نیست. تیمهایی که URI را با URL اشتباه میگیرند، معمولاً در زمان مهاجرت سرور یا تغییر دامنه، با انبوهی از آدرسهای شکسته مواجه میشوند. تیمهایی که URN را جدی میگیرند، شناسههای پایدار تولید میکنند که حتی اگر منبع جابهجا شود، هویت آن حفظ میشود. این تمایز، پایهی بسیاری از تصمیمهای بعدی در طراحی URI است.
چرا طراحی URI یک تصمیم معماری است؟
وقتی یک URI منتشر میشود، از آن لحظه به بعد بخشی از قرارداد عمومی سیستم است. حتی اگر مستندات را پاک کنید یا نسخهبندی را عوض کنید، کلاینتهای قدیمی که در گوشی کاربران نصب شدهاند، در log سرورها ثبت شدهاند و در bookmarkها ذخیره شدهاند، به همان آدرس وابسته باقی میمانند. این وابستگی، URI را از یک جزئیات پیادهسازی به یک تعهد بلندمدت تبدیل میکند.
برای درک عمیقتر، باید ابتدا خود مفهوم API را دقیق شناخت. اگر تعریف پایهای API چیست و چه کاربردی دارد برایتان روشن نیست، پیشنهاد میکنم پیش از ادامه این بخش، آن مقاله را مرور کنید. اما اگر با مفهوم آشنا هستید، نکته کلیدی این است که URI در API نقش «امضای قرارداد» را بازی میکند، نه فقط یک رشته در نوار آدرس.
سه لایه از پیامدها را باید در نظر گرفت:
لایه اول، وابستگی کلاینت. هر URI منتشرشده در SDKها، مستندات عمومی، تستهای خودکار، dashboardهای تحلیلی و logها ثبت میشود. تغییر آن، چند کانال ارتباطی را همزمان میشکند.
لایه دوم، کش و CDN. پروکسیها و CDNها بر اساس URI کلید کش میسازند. اگر ساختار URI بیثبات باشد، نرخ hit کش سقوط میکند و بار مستقیم روی دیتابیس میافتد. این نکته در بهینهسازی عملکرد REST API بهطور مستقیم بررسی شده است.
لایه سوم، تحلیل و مانیتورینگ. ابزارهای APM (Application Performance Monitoring) و لاگهای ساختاریافته بر اساس الگوی URI گروهبندی میشوند. اگر هر بار الگوی URI تغییر کند، تاریخچه متریکها از هم میگسلد.
هر URI که منتشر میشود، معادل یک تعهد عمومی است؛ هزینه تغییر آن را نه تیم شما، بلکه کلاینتهای ناشناختهای میپردازند که هرگز آنها را ندیدهاید.
در سطح معمارانه، طراحی URI سه پرسش بنیادین را پیش میکشد. نخست اینکه چه چیزی یک منبع است و چه چیزی نیست. دوم اینکه مرز بین منابع و عملیات روی آنها کجاست. سوم اینکه در چه سطحی از انتزاع، URI باقی میماند و در چه سطحی به پیادهسازی نشت میکند. پاسخ به این پرسشها، پیش از نوشتن نخستین endpoint، تعیینکننده مسیر یک دهه آینده سیستم است.
اصول طراحی URI در REST API
REST (Representational State Transfer) یک سبک معماری است که توسط Roy Fielding در پایاننامه دکتریاش معرفی شد و URI در آن نقش محوری دارد. اگر با پایههای این سبک آشنا نیستید، REST API چیست نقطه شروع درستی است. اما در سطح طراحی URI، هفت اصل عملی را باید رعایت کرد:
اصل اول، منبعمحور بودن. URI باید به یک منبع اشاره کند، نه به یک عملیات. /orders یک منبع است؛ /getOrders یک تابع است.
اصل دوم، استفاده از اسم در مسیر. مسیر URI باید از اسم تشکیل شود. عملیات توسط HTTP Method بیان میشود: GET /orders، POST /orders، DELETE /orders/42.
اصل سوم، سلسلهمراتب پایدار. ساختار مسیر باید بازتابی از روابط دامنه باشد. زیرمجموعهها بهصورت تو در تو میآیند: /users/42/orders.
اصل چهارم، استفاده از جمع. نام منابع در URI جمع بسته میشوند چون مجموعهای از نمونهها را نمایندگی میکنند: /users، /products، /invoices.
اصل پنجم، ثبات در naming convention. انتخاب بین kebab-case و snake_case اهمیت کمی دارد، اما ثبات در انتخاب اهمیت حیاتی دارد. تمام URIها باید از یک قاعده پیروی کنند.
اصل ششم، عدم افشای جزئیات پیادهسازی. URI نباید به جدول دیتابیس، نام ORM (Object-Relational Mapping) یا ساختار داخلی اشاره کند. /orders درست است؛ /tbl_order_master فاجعه است.
اصل هفتم، پایداری در طول زمان. یک URI منتشرشده نباید بدون نسخهبندی رسمی تغییر کند. اگر مجبور به تغییر هستید، مسیر جدید کنار مسیر قدیمی قرار میگیرد و مسیر قدیمی با هدر Deprecation علامتگذاری میشود.
قواعد عمیقتر این اصول در اصول طراحی REST API با مثالهای واقعی بررسی شدهاند. اما آنچه در سطح URI بیشترین خطا را تولید میکند، رعایت نکردن اصل دوم و سوم است؛ یعنی جایی که توسعهدهنده وسوسه میشود یک فعل در URI بگذارد یا ساختار سلسلهمراتبی را به نفع سادگی لحظهای نادیده بگیرد.
نامگذاری منابع؛ اسم در برابر فعل
در طراحی URI، قاعده طلایی این است که مسیر به یک منبع اشاره کند، نه به یک عملیات. وقتی مسیر /createUser یا /getUserById نوشته میشود، در واقع یک رویه RPC (Remote Procedure Call) را در قالب REST جا زدهایم و همان محدودیتهایی را دوباره ساختهایم که REST میخواست از آنها فرار کند.
مقایسه ساده این تفاوت را روشن میکند:
# نامگذاری نادرست (فعلمحور)
POST /createUser
GET /getUserById?id=42
POST /updateUserEmail
POST /deleteUserAccount
# نامگذاری درست (منبعمحور)
POST /users
GET /users/42
PATCH /users/42
DELETE /users/42
در نمونه درست، هر عملیات از طریق HTTP Method بیان میشود و مسیر همیشه به همان منبع اشاره میکند. این الگو مزیتهای مشخص دارد: کشپذیری خودکار بر اساس متد، سادگی مستندسازی، و امکان استفاده از ابزارهای عمومی REST مثل Swagger.
نکتهای که کمتر به آن توجه میشود، ارتباط بین نامگذاری URI و معنای دقیق اصطلاح API است. وقتی از واژه API استفاده میکنیم، در واقع به یک قرارداد عمومی اشاره داریم؛ و URI بخشی از این قرارداد است. اگر با ریشه و معنای دقیق این واژه آشنا نیستید، معنای اصطلاح API و ریشهشناسی آن دید روشنی از این مفهوم میدهد.
در انتخاب بین جمع و مفرد، قاعده حرفهای این است که همیشه جمع بهکار رود. دلیلش این است که هر منبع در REST نماینده یک مجموعه است، حتی اگر مجموعه فعلاً یک عضو داشته باشد. /users/42 یعنی «عضو ۴۲ از مجموعه users»، و این جملهبندی با ساختار URI هماهنگ است. برای منابع یکتا مثل تنظیمات سیستم، میتوان از مفرد استفاده کرد: /settings، /profile.
| الگو | مثال | ارزیابی |
|---|---|---|
| جمع، منبعمحور | /orders/42/items | استاندارد، توصیهشده |
| مفرد، منبعمحور | /order/42/item | قابل قبول اما غیرمعمول |
| فعلمحور | /getOrderItems | مغایر با REST |
| فعل در مسیر | /orders/42/items/create | الگوی غلط رایج |
در منابعی که ماهیت عملیاتی دارند و بهسختی در قالب CRUD (Create, Read, Update, Delete) جا میشوند، میتوان از الگوی زیرمنبع با اسم استفاده کرد. مثلاً بهجای POST /orders/42/approve، میتوان مدلسازی را در سطح دامنه بازتعریف کرد: POST /orders/42/approvals. این روش، عملیات را به یک منبع تبدیل میکند و با اصول REST سازگار میماند.
ساختار سلسلهمراتبی و مدیریت روابط
ساختار تو در توی URI بازتاب مستقیم مدل دامنه است. اگر مدل داده شما این رابطه را دارد که هر سفارش به یک کاربر تعلق دارد و هر آیتم سفارش به یک سفارش، URI منطقی /users/42/orders/7/items است. این سلسلهمراتب، خودش نوعی مستندسازی زنده است.
اما عمق زیاد سلسلهمراتب، خودش به یک تله تبدیل میشود. وقتی URI به پنج یا شش سطح میرسد، سه مشکل ظاهر میشود: کلاینت مجبور است پیش از ساخت آدرس نهایی، تمام شناسههای میانی را بداند؛ تغییر در ساختار والد، همه زیرشاخهها را میشکند؛ و امکان دسترسی مستقیم به یک زیرمنبع بدون عبور از والد از بین میرود.
قاعده عملی که در طراحی API در بکاند تجربه شده، این است که سلسلهمراتب را حداکثر در سه سطح نگه داریم. اگر نیاز به دسترسی مستقیم به یک زیرمنبع در سطح پایینتر وجود دارد، میتوان یک مسیر مسطح موازی اضافه کرد:
# سلسلهمراتبی
GET /users/42/orders/7/items/9
# مسیر مسطح موازی
GET /orders/7/items/9
GET /items/9
# مسیر مسطح مستقل (توصیهشده برای منابع پرکاربرد)
GET /orders?user_id=42
GET /items?order_id=7
این الگو، URI را از یک درخت صلب به یک گراف انعطافپذیر تبدیل میکند که هم سلسلهمراتب را حفظ میکند و هم امکان دسترسی مستقیم را میدهد. نکته کلیدی این است که بین این دو نوع مسیر، هیچکدام نباید منبع متفاوتی برگردانند؛ هر دو باید به همان رکورد دیتابیس اشاره کنند.
در طراحی روابط many-to-many، الگوی رایج این است که منبع واسط بهعنوان زیرمنبع یکی از دو طرف مدلسازی شود: /users/42/roles یا /roles/7/users. کدام سمت انتخاب میشود بستگی به این دارد که کدام سمت در دامنه غالب است و کدام سمت بیشتر از طریق API پرسیده میشود. در سیستمهایی که مدیریت نقش محور است، /roles/7/users طبیعیتر است؛ در سیستمهایی که کاربر محور است، /users/42/roles.
نسخهبندی در URI؛ چالشها و راهکارها
نسخهبندی، یکی از بحثبرانگیزترین تصمیمهای طراحی URI است. سه رویکرد اصلی وجود دارد و هر کدام مجموعهای از مزایا و هزینهها را به همراه دارد. انتخاب رویکرد درست، به مقیاس پروژه و انتظارات کلاینت بستگی دارد. جزئیات کامل این رویکردها در راهنمای نسخهبندی REST API بررسی شده است.
رویکرد اول، نسخه در مسیر (Path Versioning). رایجترین الگو است: /v1/users، /v2/users. مزیت آن صراحت و کشپذیری آسان است. عیب آن این است که نسخه در URI رسوخ میکند و در آدرسهای مستندات و bookmarkها ظاهر میشود.
رویکرد دوم، نسخه در هدر (Header Versioning). کلاینت نسخه را در هدر Accept یا یک هدر سفارشی مثل API-Version اعلام میکند. مزیت آن تمیزی URI است. عیب آن این است که تست دستی در مرورگر و کشهای عمومی ساده نیستند.
رویکرد سوم، نسخه در پارامتر پرسوجو (Query Versioning). مثل /users?version=2. این روش سادهترین است اما از نظر معماری ضعیفترین، چون نسخه بخشی از هویت منبع میشود و کش را میشکند.
# Path Versioning (توصیهشده برای APIهای عمومی)
GET /v1/users/42
GET /v2/users/42
# Header Versioning (مناسب APIهای داخلی)
GET /users/42
Accept: application/vnd.example.v2+json
# Query Versioning (توصیه نمیشود)
GET /users/42?version=2
نکته کلیدی این است که انتخاب رویکرد نسخهبندی، یک تصمیم یکباره نیست؛ بلکه باید در همان ابتدای پروژه، پیش از انتشار نخستین endpoint گرفته شود. بازگرداندن نسخهبندی به یک API منتشرشده، یکی از پرهزینهترین تغییرات معماری است.
نسخهبندی، بیمهنامه API است؛ هزینه پرداخت آن پیش از انتشار، کسری از هزینهای است که بعد از بحران پرداخت میشود.
فیلتر، مرتبسازی و صفحهبندی
در طراحی URI، بخش پرسوجو (Query String) محل بیان فیلترها، مرتبسازی و صفحهبندی است. این بخش، بخشی از هویت منبع نیست، بلکه یک نمایش خاص از آن را توصیف میکند. رعایت این تمایز، یکی از شایعترین نقاط ضعف APIهای فارسی است.
الگوی استاندارد صفحهبندی، استفاده از دو پارامتر limit و offset است:
GET /users?limit=20&offset=40
GET /products?category=electronics&sort=-created_at
GET /orders?status=shipped&from=2026-01-01&to=2026-01-31
در صفحهبندی بزرگ (Deep Pagination)، مشکل اساسی این است که offsetهای بزرگ به کوئریهای کند در دیتابیس تبدیل میشوند چون موتور دیتابیس مجبور است تمام رکوردهای قبلی را اسکن کند. راهحل حرفهای، استفاده از Cursor-based Pagination است:
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==
GET /orders?limit=50&after=2026-01-15T10:30:00Z
در Cursor-based، بهجای شمارش موقعیت، یک اشارهگر به آخرین رکورد دیدهشده ارسال میشود و کوئری بعدی از همان نقطه شروع میشود. این الگو مقیاسپذیرتر است و در APIهای بزرگ مثل GitHub و Stripe بهکار میرود.
در فیلتر، قاعده مهم این است که فیلترها نباید منبع جدیدی بسازند؛ بلکه فقط زیرمجموعهای از مجموعه اصلی برگردانند. اگر فیلتری نیاز به منطق پیچیده دارد، بهتر است به یک منبع اختصاصی تبدیل شود. مثلاً بهجای GET /orders?filter=high_value_recent، از GET /reports/high-value-orders استفاده شود.
در مرتبسازی، قاعده پرکاربرد این است که فیلد مرتبسازی با علامت منفی برای نزولی و بدون علامت برای صعودی مشخص شود: ?sort=-created_at یعنی جدیدترین اول. این الگو کوتاه، خوانا و در بیشتر فریمورکها پشتیبانی میشود.
کاراکترهای خاص و امنیت URI
URIها محل عبور دادههای حساس هستند و همین ویژگی، آنها را به یک سطح حمله تبدیل میکند. سه دسته تهدید اصلی در سطح URI قابل شناسایی است: تزریق مسیر (Path Traversal)، تزریق پارامتر (Parameter Injection)، و شمارش منابع (Resource Enumeration). در سطح بالاتر، امنیت REST API مجموعهای از اقدامات لایهای را پیشنهاد میدهد که نقطه شروع آن، همان طراحی URI است.
تهدید اول، تزریق مسیر. اگر URI شامل .. یا /های اضافی باشد و ورودی بهدرستی پاکسازی نشود، مهاجم میتواند به فایلها یا منابع خارج از محدوده دسترسی پیدا کند. مثال ساده:
GET /files/../../etc/passwd
دفاع در برابر این تهدید، ابتدا پاکسازی ورودی و سپس اعتبارسنجی الگوی URI در سطح روتینگ است، نه در سطح فایلسیستم.
تهدید دوم، تزریق پارامتر. اگر پارامترهای پرسوجو مستقیماً در کوئری دیتابیس تزریق شوند، SQL Injection رخ میدهد. دفاع استاندارد، استفاده از Prepared Statement است.
تهدید سوم، شمارش منابع. اگر از شناسههای ترتیبی مثل /users/1، /users/2 استفاده شود، مهاجم میتواند با تغییر عدد، تمام کاربران را شمارش کند. راهحل حرفهای، استفاده از UUID (Universally Unique Identifier) یا ULID (Universally Unique Lexicographically Sortable Identifier) بهجای شناسه عددی است.
# قابل شمارش
GET /users/42
# غیرقابل شمارش
GET /users/01HQZX9K2M3N4P5R6S7T8V9W0X
در پاکسازی کاراکترها، قاعده این است که فقط کاراکترهای مجاز در URI باقی بمانند. طبق RFC 3986، کاراکترهای unreserved شامل حروف، اعداد، خط تیره، زیرخط، نقطه و تیلدا هستند. هر کاراکتر دیگری باید با درصد-کدگذاری (Percent-encoding) کد شود.
# کاراکتر فاصله
GET /search?q=hello%20world
# کاراکتر اسلش در مقدار پارامتر
GET /files?path=%2Fhome%2Fuser%2Fdoc.txt
نکتهای که در بازبینیهای امنیتی زیاد دیده میشود، این است که تیمها فقط بخش ورودی کاربر را پاکسازی میکنند و بخش پارامترهای مسیر را نادیده میگیرند. در حالی که هر دو مسیر، سطح حمله یکسانی دارند. دفاع کامل، پاکسازی و اعتبارسنجی را در همه سطوح لازم میداند.
URI در GraphQL و RPC
در معماری GraphQL، طراحی URI بهکل تغییر میکند. برخلاف REST که هر منبع URI اختصاصی خود را دارد، GraphQL معمولاً از یک endpoint واحد استفاده میکند و نوع عملیات در بدنه درخواست مشخص میشود. مقایسه دقیق این دو رویکرد در مقایسه GraphQL و REST با مثالهای عملی بررسی شده است.
# REST: منابع متفاوت
GET /users/42
GET /users/42/orders
POST /orders
# GraphQL: endpoint واحد
POST /graphql
{
user(id: 42) { name, orders { id, total } }
}
در RPC (Remote Procedure Call) نیز وضعیت مشابه است. مسیر مستقیماً یک تابع را صدا میزند: POST /rpc/createUser. این الگو برای عملیاتهایی که در قالب REST جا نمیشوند مناسب است، اما برای منابع استاندارد، طراحی REST همچنان انتخاب اول است.
در پروژههای واقعی، ترکیب این سه سبک رایج است. یک API بالغ ممکن است منابع اصلی را در REST ارائه کند، عملیات پیچیده و ترکیبی را در GraphQL، و توابع زیرساختی را در RPC. تصمیم درست، بستگی به ماهیت عملیات و انتظارات کلاینت دارد.
دامهای پنهان در طراحی URI
در بازبینی صدها API در سالهای گذشته، دامهای مشخصی بارها تکرار شدهاند. فهرست کوتاهی از این دامها:
دام اول، نبود قاعده ثابت. در یک API، هم /users و هم /User و هم /user_list دیده میشود. نبود قاعده ثابت، هزینه یادگیری کلاینت را چند برابر میکند و ابزارهای تولید خودکار SDK را از کار میاندازد.
دام دوم، افشای نسخه در هدر یا مسیر. نسخهبندی باید صریح باشد، اما انتخاب رویکرد درست اهمیت دارد. نسخه در مسیر، سادهترین و پرکاربردترین گزینه است؛ نسخه در هدر، انعطافپذیرتر اما پیچیدهتر.
دام سوم، استفاده از فعل در مسیر. الگوهایی مثل /getUser، /deleteOrder، /updateProduct نشان میدهند که تیم در حال نوشتن RPC است، نه REST.
دام چهارم، عمق زیاد سلسلهمراتب. URIهایی با پنج یا شش سطح، نگهداری را پیچیده و کش را بیاثر میکنند.
دام پنجم، شناسههای ترتیبی. افشای حجم داده و امکان شمارش منابع، دو مشکل امنیتی است که با UUID بهسادگی رفع میشود.
دام ششم، نبود صفحهبندی در مجموعهها. هر endpoint که مجموعه برمیگرداند، باید صفحهبندی داشته باشد، حتی اگر فعلاً داده کم است.
دام هفتم، بیتوجهی به Caching. اگر URIها بهدرستی طراحی نشوند، کش HTTP کار نمیکند. این موضوع در اشتباهات رایج در REST API بهطور مستقیم بررسی شده است.
دام هشتم، نمایش جزئیات پیادهسازی. URIهایی مثل /wp_posts_v2 یا /tbl_order_detail نهفقط زشت هستند، بلکه به مهاجم اطلاعات ساختار داخلی میدهند.
دام نهم، پاسخ متفاوت برای منبع یکسان از دو مسیر. اگر /users/42/orders و /orders?user_id=42 پاسخهای متفاوتی بدهند، کلاینتها گیج میشوند و cache نمیتواند تصمیم درست بگیرد.
دام دهم، نبود مستندسازی. حتی بهترین طراحی URI، بدون مستندات دقیق، بیفایده است. ابزارهایی مثل Swagger این نیاز را برطرف میکنند. اگر با این ابزار آشنا نیستید، مستندسازی REST API با Swagger نقطه شروع مناسبی است.
تست و اعتبارسنجی URI
طراحی URI، مانند هر قرارداد دیگر، نیاز به تست خودکار دارد. سه سطح از تست را باید در نظر گرفت:
سطح اول، تست قرارداد. بررسی میکند که هر URI منتشرشده، همچنان به همان منبع اشاره میکند. اگر این تست شکست بخورد، یعنی یک breaking change رخ داده است.
def test_user_endpoint_contract():
response = client.get('/users/42')
assert response.status_code == 200
assert 'id' in response.json()
assert response.json()['id'] == 42
سطح دوم، تست نامگذاری. بررسی میکند که تمام URIها از یک قاعده پیروی میکنند. این تست را میتوان با آنالیز خودکار روتینگ پیاده کرد:
import re
from django.urls import get_resolver
PATTERN = re.compile(r'^/[a-z0-9\-]+$')
def validate_all_routes():
resolver = get_resolver()
for pattern in resolver.url_patterns:
path = str(pattern.pattern)
assert PATTERN.match(path) or path.endswith('/'), \
f'Invalid route: {path}'
سطح سوم، تست امنیت. بررسی میکند که کاراکترهای خاص در URI، رفتار پیشبینیشده دارند و منجر به تزریق نمیشوند:
def test_path_traversal_blocked():
response = client.get('/files/../../etc/passwd')
assert response.status_code in (400, 404)
این سه سطح تست، بخشی از یک چارچوب بزرگتر تست API هستند. اگر با ابزارهای تست حرفهای آشنا نیستید، درک عمیق REST از پایه تا طراحی حرفهای چارچوب کاملی از ابزارها و روشها ارائه میدهد.
پرسشهای پرتکرار درباره طراحی URI
URI و URL چه تفاوتی دارند؟ URI ابرمفهوم است و هر رشتهای که یک منبع را یکتا شناسایی کند. URL زیرمجموعهای از URI است که محل دسترسی منبع را هم مشخص میکند.
آیا باید از نسخهبندی در URI استفاده کرد؟ برای APIهای عمومی، بله. الگوی /v1/users رایجترین و قابلفهمترین است. برای APIهای داخلی، نسخهبندی در هدر گزینه سبکتری است.
جمع یا مفرد؟ همیشه جمع، حتی برای منابع یکتا. /users، نه /user. استثنا، منابعی مثل /settings یا /profile هستند که ماهیت یکتا دارند.
آیا میتوان در URI از فعل استفاده کرد؟ در REST، خیر. عملیات باید از طریق HTTP Method بیان شود. اما در مواردی که عملیات در قالب CRUD جا نمیشود، میتوان آن را به یک زیرمنبع با اسم تبدیل کرد.
چرا UUID بهتر از شناسه عددی است؟ چون از شمارش منابع جلوگیری میکند و افشای حجم داده را متوقف میسازد. با این حال، UUID در حجم بالا فضای بیشتری مصرف میکند و ایندکس را سنگینتر میکند.
صفحهبندی offset-based یا cursor-based؟ برای مجموعههای کوچک، offset کافی است. برای مجموعههای بزرگ یا جریانهای داده، cursor-based انتخاب درست است.
چطور از تزریق مسیر جلوگیری کنیم؟ با پاکسازی ورودی، اعتبارسنجی الگوی URI در سطح روتینگ، و محدودسازی دسترسی فایلسیستم به پوشههای مشخص.
آیا URI باید حتماً کوچک باشد؟ طول URI محدودیت عملی دارد (حدود ۲۰۰۰ کاراکتر در بیشتر مرورگرها و سرورها)، اما در عمل، URIهای طولانی نشانهای از طراحی ضعیف هستند. اگر URI بیش از چند ده کاراکتر شد، احتمالاً دادهای که باید در بدنه باشد، در مسیر قرار گرفته است.
چطور URIهای قدیمی را deprecate کنیم؟ با هدر Deprecation، مستندسازی واضح، و یک دوره گذار که در آن هر دو مسیر فعال باشند.
آیا استفاده از زیرخط مجاز است؟ در URI، زیرخط مجاز است اما خط تیره متداولتر است. نکته مهم، ثبات است؛ نه انتخاب یکی از این دو.
چطور در URI از کاراکترهای فارسی استفاده کنیم؟ بهصورت مستقیم توصیه نمیشود. کاراکترهای غیرASCII باید درصد-کدگذاری شوند یا به لاتین ترنسلیت شوند. برای منابعی که نام فارسی دارند، استفاده از شناسه لاتین و ذخیره نام فارسی در بدنه پاسخ توصیه میشود.
آیا URI میتواند شامل داده حساس باشد؟ خیر. URIها در log سرورها، مرورگرها و پروکسیها ثبت میشوند. هر داده حساس باید در هدر یا بدنه درخواست قرار بگیرد، نه در URI.
پرسشی که باید قبل از طراحی URI بعدی پاسخ دهید
پیش از آنکه نخستین endpoint بعدی را طراحی کنید، یک پرسش را از خودتان بپرسید: «اگر سه سال بعد، تیم دیگری بخواهد همین API را بازبینی کند، آیا میتواند از روی URIها، ساختار دامنه را بازسازی کند؟» اگر پاسخ منفی است، یعنی طراحی URI شما بهقدر کافی گویا نیست. اگر پاسخ مثبت است، یعنی به یک قرارداد پایدار رسیدهاید که میتواند سالها بدون تغییر بماند.
در نهایت، طراحی URI ترکیبی از اصول فنی، تجربه عملی و پیشبینی آینده است. هر تصمیم کوچک، در مقیاس هزاران endpoint، به یک الگوی بزرگ تبدیل میشود. اگر تجربهای از طراحی URI در پروژه واقعی داشتهاید — مثلاً جایی که یک تصمیم ساده سالها بعد به یک بحران تبدیل شده — برایم جالب است بدانید. مخصوصاً اگر راهحلی برای یک سناریوی خاص پیدا کردهاید که میتواند برای تیمهای دیگر هم مفید باشد. تجربه خودتان را در دیدگاهها بنویسید؛ همان راهحلها میتوانند به خواننده بعدی کمک کنند.