Back to Blog
TaroMini ProgramsCross-Platform

Designing a Cross-Platform Request Layer with Taro

Build a unified HTTP request layer in Taro for consistent API calls across Web and mini-programs

Author: ekent·Published on January 31, 2026

When building cross-platform mini-programs with Taro, the request layer is the first infrastructure problem to solve. Web uses fetch or axios, mini-programs use Taro.request—different APIs, different capabilities, different constraints. We built a unified request layer so business code doesn't need to know which platform it's running on.

Platform Differences

FeatureWeb (H5)WeChat Mini Program
Request APIfetch / XMLHttpRequestwx.request
CookiesAutomaticNot supported
CORSRequiredN/A (but domains must be whitelisted)
ConcurrencyUnlimitedMax 10
HTTPSOptionalRequired

These differences mean you can't simply use the same HTTP library everywhere. Taro provides Taro.request as a cross-platform wrapper, but it's quite basic—no interceptors, no auto-retry.

Basic Wrapper

// lib/request.ts
import Taro from '@tarojs/taro';

interface RequestConfig {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  data?: object;
  headers?: Record<string, string>;
}

interface ApiResponse<T = any> {
  code: number;
  data: T;
  message: string;
}

const BASE_URL = getBaseUrl();

function getBaseUrl(): string {
  if (process.env.NODE_ENV === 'development') {
    return 'https://dev-api.example.com';
  }
  return 'https://api.example.com';
}

async function request<T>(config: RequestConfig): Promise<T> {
  const token = Taro.getStorageSync('access_token');

  const response = await Taro.request({
    url: `${BASE_URL}${config.url}`,
    method: config.method || 'GET',
    data: config.data,
    header: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...config.headers,
    },
  });

  return handleResponse<T>(response);
}

Response Handling and Error Dispatch

Taro.request has a quirk: HTTP 4xx/5xx status codes do not throw exceptions. It only rejects on network errors. You must check status codes manually:

function handleResponse<T>(response: Taro.request.SuccessCallbackResult): T {
  const { statusCode, data } = response;

  if (statusCode === 401) {
    handleUnauthorized();
    throw new Error('Unauthorized');
  }

  if (statusCode === 404) {
    throw new Error('Resource not found');
  }

  if (statusCode >= 400) {
    const message = (data as any)?.message || `Request failed (${statusCode})`;
    Taro.showToast({ title: message, icon: 'none' });
    throw new Error(message);
  }

  const result = data as ApiResponse<T>;
  return result.data;
}

This is a major pitfall—many developers wrap Taro.request in try/catch expecting to catch 400 errors, but they never fire.

401 Handling: Login Redirect

Mini-program navigation differs from the web—you can't use window.location.href:

let isRedirecting = false;

function handleUnauthorized() {
  if (isRedirecting) return;
  isRedirecting = true;

  Taro.removeStorageSync('access_token');

  if (process.env.TARO_ENV === 'weapp') {
    Taro.redirectTo({ url: '/pages/login/index' });
  } else {
    window.location.href = '/login';
  }

  setTimeout(() => { isRedirecting = false; }, 3000);
}

process.env.TARO_ENV is a compile-time constant that Taro replaces with the target platform identifier during build, so no dead code is included.

Request Retry

Mini-program network conditions are less stable than desktop browsers. For idempotent GET requests, add automatic retry:

async function requestWithRetry<T>(
  config: RequestConfig,
  retries = 2
): Promise<T> {
  for (let i = 0; i <= retries; i++) {
    try {
      return await request<T>(config);
    } catch (error) {
      if (i === retries) throw error;

      // Only retry on network errors, not business errors
      if (error instanceof Error && error.message.includes('request:fail')) {
        await sleep(1000 * (i + 1)); // Incremental backoff
        continue;
      }
      throw error;
    }
  }
  throw new Error('Unreachable');
}

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

The key is retrying only on network errors (request:fail is Taro's network error marker). Business errors (like invalid parameters) won't be fixed by retrying.

Loading State Management

Mini-programs typically use Taro.showLoading for loading indicators. But with concurrent requests, the first one to complete hides the loading while others are still in progress:

let loadingCount = 0;

function showLoading() {
  if (loadingCount === 0) {
    Taro.showLoading({ title: 'Loading...' });
  }
  loadingCount++;
}

function hideLoading() {
  loadingCount--;
  if (loadingCount <= 0) {
    loadingCount = 0;
    Taro.hideLoading();
  }
}

Reference counting ensures the loading indicator stays visible until all requests complete.

Convenience Exports

export const http = {
  get: <T>(url: string, data?: object) =>
    request<T>({ url, method: 'GET', data }),

  post: <T>(url: string, data?: object) =>
    request<T>({ url, method: 'POST', data }),

  put: <T>(url: string, data?: object) =>
    request<T>({ url, method: 'PUT', data }),

  del: <T>(url: string, data?: object) =>
    request<T>({ url, method: 'DELETE', data }),
};

Business code becomes:

const userList = await http.get<User[]>('/api/users');
await http.post('/api/orders', { productId: 123 });

Takeaways

The core challenge of a Taro cross-platform request layer is smoothing over platform differences: Taro.request's non-throwing behavior, token storage mechanisms, navigation APIs, and network reliability. Once this layer is solid, business development is just http.get / http.post—no need to care whether the code runs in a browser or a mini-program.