به محتوای اصلی

اتصال فرانت‌اند به API؛ با fetch خام، axios یا کلاینت اختصاصی؟

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

عاطفه امیری۴ دقیقه مطالعه
در این مطلب

هر اپلیکیشنی بالاخره به این خط می‌رسد: باید داده را از یک API بگیرد. سؤال این است که آن لایه را چطور بنویسید — با fetch خام، با axios، یا با کلاینتی که مخصوص همان API نوشته شده. این مقاله چهار چیزی را نشان می‌دهد که در نوشتن دستی این لایه خراب می‌شوند، و بعد سه گزینه را با هم مقایسه می‌کند.

گزینه‌ی اول: fetch خام

داخل مرورگر و Node نسخه‌ی ۱۸ به بالا از قبل هست و هیچ وابستگی‌ای اضافه نمی‌کند. برای یک درخواست ساده کاملاً کافی است:

const res = await fetch("https://api.example.com/products");
const data = await res.json();

مشکل از جایی شروع می‌شود که اپ بزرگ شود. fetch برای پاسخ ۴۰۴ یا ۵۰۰ خطا پرتاب نمی‌کند — فقط res.ok را false می‌کند. اگر یادتان برود چکش کنید، برنامه با یک آبجکت خطا به‌جای داده ادامه می‌دهد و باگ چند لایه بعد ظاهر می‌شود.

گزینه‌ی دوم: axios

خطاها را خودش پرتاب می‌کند، اینترسپتور دارد و JSON را خودکار پارس می‌کند. کتابخانه‌ی خوبی است و دلیل محبوبیتش روشن است.

ولی axios فقط لایه‌ی HTTP را حل می‌کند، نه چیزی که مخصوص API شماست: باز هم باید خودتان آدرس‌ها را بسازید، پارامترها را انکود کنید، و منطق توکن را بنویسید.

چهار چیزی که در نوشتن دستی خراب می‌شود

اینها را هم با fetch می‌بینید هم با axios، چون هیچ‌کدام از API شما خبر ندارند.

انکود کردن پارامترها

یک فیلتر ساده مثل «قیمت بیشتر از ۵۰»، بسته به قرارداد API، در URL این شکلی می‌شود:

?filter%5Bprice%5D%5Bgt%5D=50

دستی نوشتنش جواب می‌دهد تا روزی که مقدار فیلتر یک رشته‌ی فارسی، یک تاریخ، یا خودش شامل & باشد. آن‌وقت باگی دارید که فقط روی بعضی داده‌ها ظاهر می‌شود.

انقضای توکن وسط درخواست‌های موازی

این پرهزینه‌ترین است و در محیط توسعه تقریباً هرگز خودش را نشان نمی‌دهد.

توکن دسترسی عمر کوتاهی دارد. فرض کنید صفحه‌ای باز می‌شود که هم‌زمان سه درخواست می‌فرستد و توکن همان لحظه منقضی شده. هر سه ۴۰۱ می‌گیرند، و اگر منطق ساده‌ای نوشته باشید، هر سه هم‌زمان تلاش می‌کنند توکن را تازه کنند.

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

راه‌حلش این است که همه‌ی درخواست‌های ۴۰۱ یک عملیات رفرش مشترک را منتظر بمانند، نه اینکه هرکدام یکی بسازند. نوشتنش سخت نیست، ولی باید بدانید که لازم است.

خطای بی‌ساختار

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

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

صفحه‌بندی با شماره‌ی صفحه

اگر با page=2 ورق بزنید و همان لحظه رکورد تازه‌ای اضافه شود، مرز صفحه‌ها جابه‌جا می‌شود: یک رکورد را دوبار می‌بینید یا یکی را کامل جا می‌اندازید.

صفحه‌بندی مبتنی بر نشانگر (cursor) این مشکل را ندارد، چون به‌جای «از رکورد بیستم» می‌گوید «از این‌جا به بعد». اگر API شما نشانگر می‌دهد، از همان استفاده کنید.

گزینه‌ی سوم: کلاینت مخصوص همان API

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

fetch خامaxiosکلاینت اختصاصی
وابستگی اضافهندارددارددارد
خطا را خودش پرتاب می‌کندنهبلهبله
ساخت آدرس و انکود فیلتردستیدستیآماده
مدیریت و تمدید توکندستیبا اینترسپتورآماده
تایپ پاسخ‌هادستیدستیمعمولاً آماده

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

نمونه‌ی عملی

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

نکته‌ای که ارزش دیدن دارد، بدون توجه به اینکه از کدام سرویس استفاده می‌کنید: جای نگه‌داری توکن. پیش‌فرض این کتابخانه حافظه است نه localStorage، چون localStorage برای هر اسکریپتی روی صفحه خواندنی است و یک آسیب‌پذیری XSS یعنی یک رفرش‌توکن دزدیده‌شده. ماندگار کردن نشست باید تصمیم صریح خودتان باشد:

import { createClient, browserStorage } from "@fikaro/client";

const client = createClient({
  project: "my-app",
  storage: browserStorage(),   // ماندگار بین رفرش‌ها — با آگاهی از ریسکش
});

برای اپ موبایل، expo-secure-store یا کی‌چین سیستم‌عامل. برای وبِ حساس، کوکی httpOnly از سمت سرور خودتان.

جمع‌بندی

اگر پروژه کوچک است fetch خام بنویسید و وابستگی اضافه نکنید. اگر متوسط است و فقط لایه‌ی HTTP اذیتتان می‌کند، axios کافی است. اگر API‌ای که مصرف می‌کنید کلاینت رسمی دارد و آن چهار مورد را حل کرده، استفاده از آن وقت شما را برای منطق واقعی اپ آزاد می‌کند.

آنچه در هر سه حالت باید بدانید همان چهار چیز است: انکود پارامتر، رفرش مشترک توکن، ساختار خطا، و صفحه‌بندی. نوشتن یا نوشتنشان دست شماست؛ نادیده گرفتنشان نه.

اگر لایه‌ی احراز هویت را از صفر می‌سازید، مقاله‌ی JWT همان بخش را کامل‌تر باز کرده است.

سوالات متداول

برای اتصال به API از fetch استفاده کنم یا axios؟
برای پروژه‌ی کوچک، fetch خام کافی است و وابستگی اضافه نمی‌کند. axios وقتی می‌ارزد که پرتاب خودکار خطا و اینترسپتور را بخواهید. هیچ‌کدام مسائل مخصوص API شما مثل ساخت فیلتر و مدیریت توکن را حل نمی‌کنند.
چرا کاربر وسط کار از حساب خارج می‌شود؟
معمولاً به این دلیل که چند درخواست هم‌زمان با توکن منقضی روبه‌رو شده‌اند و هرکدام جداگانه تلاش کرده‌اند توکن را تازه کنند. چون رفرش‌توکن چرخشی است، فقط اولی موفق می‌شود و بقیه نشست را باطل می‌کنند. راه‌حل این است که همه‌ی درخواست‌ها یک عملیات رفرش مشترک را منتظر بمانند.
توکن را در localStorage نگه دارم؟
فقط با آگاهی از ریسکش. localStorage برای هر اسکریپتی روی صفحه خواندنی است، پس یک آسیب‌پذیری XSS یعنی رفرش‌توکن دزدیده شده. برای اپ حساس، کوکی httpOnly از سمت سرور خودتان یا حافظه‌ی امن سیستم‌عامل در موبایل گزینه‌ی درست‌تری است.
صفحه‌بندی با شماره‌ی صفحه چه مشکلی دارد؟
اگر وسط ورق زدن رکورد تازه‌ای اضافه یا حذف شود، مرز صفحه‌ها جابه‌جا می‌شود و کاربر یک رکورد را دوبار می‌بیند یا یکی را جا می‌اندازد. صفحه‌بندی مبتنی بر نشانگر (cursor) این مشکل را ندارد چون به موقعیت عددی وابسته نیست.
کلاینت اختصاصی یک سرویس، وابستگی اضافه نیست؟
هست، و باید ارزشش را داشته باشد. معیار سنجش این است که آیا انکود پارامتر، رفرش مشترک توکن، ساختار خطا و صفحه‌بندی را حل کرده یا فقط یک پوشش نازک روی fetch است. اگر فقط پوشش نازک است، خودتان بنویسید.

مطالب مرتبط