DEV Community

Cover image for From Client to Server: Alova 3's Full-Stack Request Strategy Practice
Eddie Kimpel
Eddie Kimpel

Posted on

From Client to Server: Alova 3's Full-Stack Request Strategy Practice

Many people may still think of alova as a lightweight request strategy library, which is certainly the core feature of alova 2. For example, consider the following code:

const { loading, data, error } = useRequest(() => alovaInstance.Get('/xxx'))
Enter fullscreen mode Exit fullscreen mode

This is a typical example of alova being used on the client side. However, alova has now been updated to version 3. Of course, these client strategies remain intact, but it's no longer limited to the client side—it can now handle server-side scenarios with ease.

Alova 3 provides server-side request strategies (server hooks) and server-side storage adapters like Redis and file storage, allowing us to conveniently implement full-chain requests and forwarding on the server side.

Let's first look at the complete flow of a request:

Client (Browser/App)
    → Node.js BFF Layer (data transformation, etc.)
    → API Gateway (authentication, rate limiting, routing, etc.)
    → Backend Microservices
Enter fullscreen mode Exit fullscreen mode

The server hooks and distributed multi-level caching provided by alova allow us to easily implement request handling at all of the above levels.

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 });
});
Enter fullscreen mode Exit fullscreen mode

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 in Redis 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, {
  // Set 3-hour cache, will be saved in Redis, subsequent requests with same parameters will hit cache
  cacheFor: {
    mode: 'restore',
    expire: 3 * 3600 * 1000
  },
  headers: {
    'x-user-id': userId,
    'Authorization': `Bearer ${token}`
  }
});

const app = express();

// Implement app to receive all requests and forward to alova
// Register routes for all HTTP methods
const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
methods.forEach(method => {
  app[method]('*', async (req, res) => {
    const { method, originalUrl, headers, body, query } = req;

    // Use alova to send request
    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');
});
Enter fullscreen mode Exit fullscreen mode

Of course, if you need to re-authenticate for every request, you can also remove cacheFor in authRequest to disable caching.

Rate Limiting Strategy

Alova's rateLimiter can implement distributed rate limiting strategies, internally using node-rate-limiter-flexible. Let's refactor the implementation:

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, indicates that when rate limit is reached, it will extend [blockDuration]ms, e.g., 5 wrong passwords in 1 hour locks for 24 hours, this 24 hours is this parameter
   */
  blockDuration: 24 * 60 * 60 * 1000
});

const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
methods.forEach(method => {
  app[method]('*', async (req, res) => {
    const { method, originalUrl, headers, body, query } = req;

    // Use rateLimit wrapper here, it will use l2Cache storage adapter by default to store control parameters, in this example it will use Redis storage adapter.
    const method = gatewayAlova.Request({
      method: method.toLowerCase(),
      url: originalUrl,
      params: query,
      data: body,
      headers
    });
    const response = await rateLimit(method, {
      key: req.ip // Use IP as tracking key to prevent frequent requests from the same IP
    });

    // ...
  });
});
Enter fullscreen mode Exit fullscreen mode

Third-Party Service Integration: Automatic Token Maintenance

Integrating with external APIs requires access_token management, and many third-party access_tokens have usage limits. Here we can use alova 3 + Redis storage adapter to implement distributed automatic lifecycle maintenance of access_tokens, where Redis is used for access_token caching, and atom hook is used for atomic operations of distributed token updates.

import { createAlova, queryCache } from 'alova';
import RedisStorageAdapter from '@alova/storage-redis';
import adapterFetch from '@alova/fetch';
import { atomize } from 'alova/server';

const redisAdapter = new RedisStorageAdapter({
  host: 'localhost',
  port: '6379',
  username: 'default',
  password: 'my-top-secret',
  db: 0
});
const thirdPartyAlova = createAlova({
  requestAdapter: adapterFetch(),
  async beforeRequest(method) {
    // Check if it's a third-party API, if so, get the token
    if (method.meta?.isThirdPartyApi) {
      // Get token in an atomic way to prevent multiple processes from getting token simultaneously
      const accessTokenGetMethod = getAccessToken();
      let accessToken = await queryCache(accessTokenGetMethod);
      if (!accessToken) {
        // Will be cached after successful retrieval
        accessToken = await atomize(accessTokenGetMethod);
      }
      method.config.params.access_token = accessToken;
    }
  },
  l2Cache: redisAdapter,
});

const getAccessToken = () => thirdPartyAlova.Get('http://third-party.com/token', {
  params: {
    grant_type: 'client_credentials',
    client_id: process.env.THIRD_PARTY_CLIENT_ID,
    client_secret: process.env.THIRD_PARTY_CLIENT_SECRET
  },
  cacheFor: {
    mode: 'restore',
    expire: 1 * 3600 * 1000 // Two hours cache time
  }
});

const getThirdPartyUserInfo = userId => thirdPartyAlova.Get('http://third-party.com/user/info', {
  params: {
    userId
  },
  meta: {
    isThirdPartyApi: true
  }
});
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

In addition to these, alova also provides distributed verification code sending and validation, request retry, and other server hooks. If you want to learn more, you can refer to Server-Side Request Strategies.

If you think alova is good, we sincerely hope you can try it out and give us a free GitHub star.

Visit the alovajs official website for more detailed information: alovajs Official Website.

If you're interested, you can join our community to get the latest updates first and communicate directly with the development team, sharing your ideas and suggestions.

If you have any questions, you can join the above groups for consultation, or post in GitHub repository Discussions. If you encounter problems, please submit them in GitHub issues, and we will resolve them as quickly as possible.

Top comments (0)