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
| Feature | Web (H5) | WeChat Mini Program |
|---|---|---|
| Request API | fetch / XMLHttpRequest | wx.request |
| Cookies | Automatic | Not supported |
| CORS | Required | N/A (but domains must be whitelisted) |
| Concurrency | Unlimited | Max 10 |
| HTTPS | Optional | Required |
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.