클라이언트에서 서버로: Alova 3의 풀스택 요청 전략 실습
Source: Dev.to
BFF 레이어에서 클라이언트 요청 전달하기
BFF 레이어에서는 클라이언트 요청을 백엔드 마이크로서비스로 전달해야 하는 경우가 자주 있습니다. async_hooks를 사용해 각 요청의 컨텍스트에 접근하고 이를 Alova의 beforeRequest에 추가하면 사용자와 관련된 데이터를 전달할 수 있습니다.
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 });
});
API 게이트웨이에서의 활용 사례
게이트웨이에서는 인증, 요청 속도 제한, 요청 분배 등이 자주 필요합니다. Alova 3의 Redis 스토리지 어댑터와 rateLimiter를 활용하면 분산 인증 서비스와 요청 속도 제한을 효과적으로 구현할 수 있습니다.
인증은 이렇게 할 수 있습니다
인증 토큰에 만료 시간이 있는 경우, 게이트웨이에 Redis 스토리지 어댑터를 설정해 토큰을 재사용하도록 저장할 수 있습니다. 단일 머신 클러스터 서비스라면 @alova/storage-file 파일 스토리지 어댑터를 사용할 수도 있습니다.
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: 매 요청마다 재인증이 필요하다면
authRequest의cacheFor옵션을 삭제해 캐싱을 비활성화하면 됩니다.
속도 제한 전략
Alova의 rateLimiter는 내부적으로 node-rate-limiter-flexible을 사용해 분산 속도 제한 전략을 구현할 수 있습니다. 아래는 모든 게이트웨이 라우트에 제한자를 적용한 리팩터링 예시입니다.
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());
});
});
서드파티 서비스 연동: 자동 토큰 관리
외부 API와 연동할 때는 access_token 수명 주기를 관리해야 하는 경우가 많습니다. Alova 3와 Redis 스토리지 어댑터, 그리고 atom 훅을 함께 사용하면 토큰을 분산 환경에서 자동으로 유지 보수할 수 있습니다.
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);
})();