From Client to Server: Alova 3's Full-Stack Request Strategy Practice
Source: Dev.to
Forwarding Client Requests in the BFF Layer
In the BFF layer, we often need to forward client requests to backend microservices. You can use async_hooks to access the context of each request and add it to the request in Alova’s beforeRequest, enabling the forwarding of user‑related data.
import { createAlova } from 'alova';
import adapterFetch from '@alova/fetch';
import express from 'express';
import { AsyncLocalStorage } from 'node:async_hooks';
// Create async local storage instance
const asyncLocalStorage = new AsyncLocalStorage();
const alovaInstance = createAlova({
requestAdapter: adapterFetch(),
beforeRequest(method) {
// Get request headers from async context and pass to downstream
const context = asyncLocalStorage.getStore();
if (context && context.headers) {
method.config.headers = {
...method.config.headers,
...context.headers,
};
}
},
responded: {
onSuccess(response) {
// Data transformation processing
return {
data: response.data,
timestamp: Date.now(),
transformed: true,
};
},
onError(error) {
console.error('Request failed:', error);
throw error;
},
},
});
const app = express();
// Set once in middleware, automatically passed throughout
app.use((req, res, next) => {
const context = {
userId: req.headers['x-user-id'],
token: req.headers['authorization'],
};
asyncLocalStorage.run(context, next);
});
// Business code focuses on business logic
app.get('/api/user-profile', async (req, res) => {
// No need to manually pass context anymore!
const [userInfo, orders] = await Promise.all([
alovaInstance.Get('http://gateway.com/user/profile'),
alovaInstance.Get('http://gateway.com/order/recent'),
]);
res.json({ user: userInfo.data, orders: orders.data });
});
Use Cases in API Gateway
In gateways, authentication, request rate limiting, and request distribution are often needed. Alova 3’s Redis storage adapter and rateLimiter can effectively implement distributed authentication services and request rate limiting.
Authentication Can Be Done Like This
If authentication tokens have an expiration time, you can configure a Redis storage adapter in the gateway to store tokens for reuse. For single‑machine cluster services, you can also use the @alova/storage-file file storage adapter.
import { createAlova } from 'alova';
import RedisStorageAdapter from '@alova/storage-redis';
import adapterFetch from '@alova/fetch';
import express from 'express';
const redisAdapter = new RedisStorageAdapter({
host: 'localhost',
port: '6379',
username: 'default',
password: 'my-top-secret',
db: 0,
});
const gatewayAlova = createAlova({
requestAdapter: adapterFetch(),
async beforeRequest(method) {
const newToken = await authRequest(
method.config.headers['Authorization'],
method.config.headers['UserId']
);
method.config.headers['Authorization'] = `Bearer ${newToken}`;
},
// Set L2 storage adapter
l2Cache: redisAdapter,
// ...
});
const authRequest = (token, userId) =>
gatewayAlova.Post('http://auth.com/auth/token', null, {
// Cache for 3 hours; subsequent requests with the same parameters will hit Redis
cacheFor: {
mode: 'restore',
expire: 3 * 3600 * 1000,
},
headers: {
'x-user-id': userId,
Authorization: `Bearer ${token}`,
},
});
const app = express;
// Forward all incoming requests to alova
const methods = [
'get',
'post',
'put',
'delete',
'patch',
'options',
'head',
];
methods.forEach((m) => {
app[m]('*', async (req, res) => {
const { method, originalUrl, headers, body, query } = req;
const response = await gatewayAlova.Request({
method: method.toLowerCase(),
url: originalUrl,
params: query,
data: body,
headers,
});
// Forward response headers
for (const [key, value] of response.headers.entries()) {
res.setHeader(key, value);
}
// Send response data
res.status(response.status).send(await response.json());
});
});
app.listen(3000, () => {
console.log('Gateway server started on port 3000');
});
Note: If you need to re‑authenticate for every request, simply remove the
cacheForoption inauthRequestto disable caching.
Rate Limiting Strategy
Alova’s rateLimiter can implement distributed rate limiting strategies, internally using node-rate-limiter-flexible. Below is a refactored implementation that applies the limiter to all gateway routes.
import { createRateLimiter } from 'alova/server';
const rateLimit = createRateLimiter({
/**
* Time for points reset, in ms
* @default 4000
*/
duration: 60 * 1000,
/**
* Maximum consumable quantity within duration
* @default 4
*/
points: 4,
/**
* Namespace, prevents conflicts when multiple rateLimits use the same storage
*/
keyPrefix: 'user-rate-limit',
/**
* Lock duration in ms; when the limit is reached, the block lasts this long.
*/
blockDuration: 24 * 60 * 60 * 1000,
});
const methods = [
'get',
'post',
'put',
'delete',
'patch',
'options',
'head',
];
methods.forEach((m) => {
app[m]('*', async (req, res) => {
const { method, originalUrl, headers, body, query } = req;
const alovaRequest = gatewayAlova.Request({
method: method.toLowerCase(),
url: originalUrl,
params: query,
data: body,
headers,
});
const response = await rateLimit(alovaRequest, {
key: req.ip, // Use IP as tracking key to prevent frequent requests from the same IP
});
// Forward response (similar to the previous example)
for (const [key, value] of response.headers.entries()) {
res.setHeader(key, value);
}
res.status(response.status).send(await response.json());
});
});
Third‑Party Service Integration: Automatic Token Maintenance
Integrating with external APIs often requires managing access_token lifecycles. Using Alova 3 together with the Redis storage adapter and the atom hook, you can implement distributed automatic maintenance of access tokens.
import { createAlova, queryCache, atom } from 'alova';
import RedisStorageAdapter from '@alova/storage-redis';
import adapterFetch from '@alova/fetch';
// Configure Redis for token caching
const redisAdapter = new RedisStorageAdapter({
host: 'localhost',
port: 6379,
});
const alovaInstance = createAlova({
requestAdapter: adapterFetch(),
l2Cache: redisAdapter,
});
// Atomic operation to refresh token only once across distributed instances
const refreshTokenAtom = atom(async (currentToken) => {
if (currentToken && Date.now() {
const res = await requestWithToken({
method: 'GET',
url: 'https://thirdparty.com/api/resource',
});
console.log(res.data);
})();