All Products
Search
Document Center

CDN:Use Alibaba Cloud CDN to build a high-speed GraphQL gateway

Last Updated:Jan 15, 2024

This topic describes how to use EdgeRoutine (ER) and GraphQL to build a high-speed API gateway. ER is an edge serverless solution that is deployed on Alibaba Cloud CDN points of presence (POPs). In this topic, GraphQL works with ER to act as a gateway for a weather forecast website. You can also use the process that is described in this topic to develop a GraphQL gateway by using Dynamic Content Delivery Network (DCDN).

Background information

As Internet technologies continue to advance at a rapid rate, an increasing number of enterprises around the world are using GraphQL to provide APIs. Similarly, Alibaba Cloud technical teams exclusively use GraphQL to describe, publish, and call APIs. Other world-leading enterprises such as Facebook, Netflix, GitHub, PayPal, Microsoft, Volkswagen, and Walmart also use GraphQL. A worldwide survey shows that GraphQL is popular among frontend developers. GraphQL is suitable for the gateway layer in the Backend-for-Frontend (BFF) pattern. GraphQL allows users to integrate and encapsulate APIs, such as High-speed Service Framework (HSF) APIs and third-party RESTful APIs, into a self-managed Service Facade to meet specific business requirements. GraphQL can be easily integrated with HTTP-based gateways, such as RESTful, MTOP, and MOPEN. GraphQL is also suitable for the gateway layer in the serverless or FaaS architecture. You can call all GraphQL APIs by using only one HTTP trigger.

GraphQL is a query language for APIs and a runtime for executing queries by using existing data. GraphQL provides a complete and understandable description of the data in your API, which allows clients to request only the information they require. This helps advance APIs and allows the development of powerful developer tools. For more information about GraphQL, visit the GraphQL official website.

Integrate ER with GraphQL

ER is a serverless computing platform developed by the Alibaba Cloud CDN technical team. ER provides a Service Worker container that adheres to standards similar to W3C. ER uses the idle computing resources and robust acceleration and caching capabilities of Alibaba Cloud POPs around the world to provide highly available, high-performance, distributed and elastic computing services. For more information about ER, see What is EdgeRoutine?

让CDN成为高性能GraphQL网关

You can use GraphQL as the BFF gateway layer, which can handle a large number of query requests, and the responses for read-only query requests remain unchanged over time.

In the preceding figure, ER is used as the proxy for the GraphQL query requests. The first time that a query request is initiated, the system directs the request from ER to the GraphQL gateway layer and then directs the query request from the GraphQL gateway layer to the specified application by using HSF. The system obtains the requested content and caches the content on POPs. The system determines whether the subsequent query requests are directed to ER or the GraphQL gateway layer based on the TTL. If the requested content is cached on POPs, the number of queries per second (QPS) for your application significantly decreases.

Port Apollo GraphQL Server

Apollo GraphQL Server is a widely used open-source GraphQL service. The Node.js version is used by applications that are designed based on the BFF pattern at scale. Apollo GraphQL Server is a Node.js-oriented project. However, ER provides a serverless container that is similar to Service Worker. Apollo GraphQL Server must be ported to ER.

Step 1: Build a development environment and scaffolding in the TypeScript language

Build a TypeScript development environment in the ER container. Use the TypeScript library of Service Worker to simulate the compilation environment. Use Webpack as the on-premises debugging server, and use Service Worker in your browser to run the edge.js script. Then, enable network communications based on the Webpack socket to implement hot reloading.

Step 2: Establish connections to HTTP servers

The following sample code provides an example on how to import the ApolloServerBase class and establish connections to HTTP servers to create an Apollo GraphQL Server that is suitable for the ER container.

import { ApolloServerBase } from 'apollo-server-core';
import { handleGraphQLRequest } from './handlers';
/**
 * Port Apollo GraphQL Server to ER. 
 */
export class ApolloServer extends ApolloServerBase {
  /**
   * Listen to GraphQL Post requests on the specified path. 
   * @param path The path to which you want to listen. 
   */
  async listen(path = '/graphql') {
    // If the listen() method is incorrectly used before the start() method is called, an exception occurs. 
    this.assertStarted('listen');
    // addEventListenr('fetch', (FetchEvent) => void) is provided by ER. 
    addEventListener('fetch', async (event: FetchEvent) => {
      // Listen to all requests from ER. 
      const { request } = event;
      if (request.method === 'POST') {
        // Process only POST requests.
        const url = new URL(request.url);
        if (url.pathname === path) {
          // If the path meets your requirements, use the handleGraphQLRequest() method to process the requests.
          const options = await this.graphQLServerOptions();
          event.respondWith(handleGraphQLRequest(this, request, options));
        }
      }
    });
  }
}

Step 3: Execute the handleGraphQLRequest() method

The handleGraphQLRequest() method works in channel mode. The method converts HTTP requests into GraphQL requests. Apollo GraphQL Server receives the GraphQL requests and returns GraphQL responses. Then, the method converts the GraphQL responses into HTTP responses. Apollo natively provides the runHttpQuery() method, which is similar to the handleGraphQLRequest() method. However, the runHttpQuery() method requires the use of built-in modules such as buffer in the Node.js environment. This method cannot be compiled in the Service Worker environment. The following sample code provides an example on how to execute the handleGraphQLRequest() method:

import { GraphQLOptions, GraphQLRequest } from 'apollo-server-core';
import { ApolloServer } from './ApolloServer';
/**
 * Parse the GraphQL query from an HTTP request and execute the query. Then, return the execution result. 
 */
export async function handleGraphQLRequest(
  server: ApolloServer,
  request: Request,
  options: GraphQLOptions,
): Promise<Response> {
  let gqlReq: GraphQLRequest;
  try {
    // Parse a JSON-formatted request from the HTTP request body. 
    // The request is of the GraphQL type and contains the query, variables, and operationName. 
    gqlReq = await request.json();
  } catch (e) {
    throw new Error('Error occurred when parsing request body to JSON.');
  }
  // Execute the GraphQL request. 
  // If the execution fails, no exception occurs. However, a response that contains 'errors' is returned. 
  const gqlRes = await server.executeOperation(gqlReq);
  const response = new Response(JSON.stringify({ data: gqlRes.data, errors: gqlRes.errors }), {
    // Make sure that content-type is in the JSON format. 
    headers: { 'content-type': 'application/json' },
  });
  // Copy the message headers in the GraphQL response to an HTTP response. 
  for (const [key, value] of Object.entries(gqlRes.http.headers)) {
    response.headers.set(key, value);
  }
  return response;
}

Example: Weather forecast by using GraphQL developed based on Alibaba Cloud CDN

This example describes the high-speed GraphQL gateway built on top of Alibaba Cloud CDN for tianqiapi.com. Secondary encapsulation is performed on a third-party weather service and is delivered by using this gateway.

tianqiapi.com specifies limits on the QPS quota for non-paying users. Users can query weather conditions from tianqiapi.com up to 300 times per day. In most cases, weather forecasts do not frequently change. The first time that a user initiates a request to query weather conditions in a specific city, the request is directed to tianqiapi.com. Subsequent requests to query the weather conditions in the same city are directed to the nearest POP on which the weather data is cached.

Overview of tianqiapi.com

tianqiapi.com provides weather forecast services and handles tens of millions of QPS per day. The following code provides a sample HTTP request that is initiated to query the weather conditions in a specific city. In this example, Nanjing is used.

HTTP request

Request URL: https://www.tianqiapi.com/free/day?appid={APP_ID}&appsecret={APP_SECRET}&city=%E5%8D%97%E4%BA%AC
Request Method: GET
Status Code: 200 OK
Remote Address: 127.0.0.1:7890
Referrer Policy: strict-origin-when-cross-origin
Note

{APP_ID} and {APP_SECRET} specify your API account.

HTTP response

HTTP/1.1 200 OK
Server: nginx
Date: Thu, 19 Aug 2021 06:21:45 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Content-Encoding: gzip
{
  air: "94",
  city: "Nanjing",
  cityid: "101190101",
  tem: "31",
  tem_day: "31",
  tem_night: "24",
  update_time: "14:12",
  Wea: "Cloudy",
  wea_img: "yun",
  win: "Southeast wind",
  win_meter: "9km/h",
  win_speed: "Level 2"
}

API operation

export async function fetchWeatherOfCity(city: string) {
  // The URL class is implemented in ER. 
  const url = new URL('http://www.tianqiapi.com/free/day');
  // Use the free account that is provided by tianqiapi. 
  url.searchParams.set('appid', '2303****');
  url.searchParams.set('appsecret', '8Yvl****');
  url.searchParams.set('city', city);
  const response = await fetch(url.toString);
  return response;
}

Step 1: Compile a custom schema in the GraphQL SDL language

The following sample code provides an example on how to use the GraphQL SDL language to define the schema of the API that you want to implement:

type Query {
    "Query the version information of the current API. "
  versions: Versions!
    "Query real-time weather data in the specified city."
  weatherOfCity(name: String!): Weather!
}
"""
City information
"""
type City {
  """
  The unique identifier of the city.
  """
  id: ID!
  """
  The name of the city.
  """
  name: String!
}
"""
Version information
"""
type Versions {
  """
  The version number of the API. 
  """
  api: String!
  """
  'graphql' The NPM version number. 
  """
  graphql: String!
}
"""
Weather data
"""
type Weather {
  "Current city"
  city: City!
  "Last update time"
  updateTime: String!
  "Weather condition code"
  code: String!
  "Weather status in Chinese"
  localized: String!
  "Daytime temperature"
  tempOfDay: Float!
  "Night temperature"
  tempOfNight: Float!
}

Step 2: Deploy a GraphQL resolver

The following sample code provides an example on how to deploy a GraphQL resolver:

import { version as graphqlVersion } from 'graphql';
import { apiVersion } from '../api-version';
import { fetchWeatherOfCity } from '../tianqi-api';
export function versions() {
  return {
    // Compared with FaaS, ER has a slower deployment speed. 
    // Therefore, you need to manually modify the version number in the api-version.ts file before each deployment. 
    // If the version number returned by the query changes, the GraphQL resolver is deployed on POPs. 
    api: apiVersion,
    graphql: graphqlVersion,
  };
}
export async function weatherOfCity(parent: any, args: { name: string }) {
  // Call the API and change the format of the response to JSON. 
  const raw = await fetchWeatherOfCity(args.name).then((res) => res.json());
  // Map the original response to the API that is defined. 
  return {
    city: {
      id: raw.cityid,
      name: raw.city,
    },
    updateTime: raw.update_time,
    code: raw.wea_img,
    localized: raw.wea,
    tempOfDay: raw.tem_day,
    tempOfNight: raw.tem_night,
  };
}

Step 3: Create and start a server

Create a server object. Then, start and configure the server object to listen to the graphql path.

// Note that "import {ApolloServer} from apollo-server" is no longer used. 
import { ApolloServer } from '@ali/apollo-server-edge-routine';
import { default as typeDefs } from '../graphql/schema.graphql';
import * as resolvers from '../resolvers';
// Create a server.
const server = new ApolloServer({
  // typeDefs is a DocumentNode object of GraphQL. 
  // The graphql file becomes a DocumentNode object after the file is loaded by webpack-graphql-loader. 
  typeDefs,
  // The resolver in Step 2.
  resolvers,
});
// Start the server and listen to the specified path. You need to only write one line of code. 
server.start().then(() => server.listen());

Step 4: Add polyfills

To ensure that the code written in the ER environment can be read by TypeScript, lib and types must be specified in tsconfig.json.

{
  "compilerOptions": {
    "alwaysStrict": true,
    "esModuleInterop": true,
    "lib": ["esnext", "webworker"],
    "module": "esnext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "preserveConstEnums": true,
    "removeComments": true,
    "sourceMap": true,
    "strict": true,
    "target": "esnext",
    "types": ["@ali/edge-routine-types"]
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

Compared with applications that use the serverless or FaaS architecture, the application used in this example does not run in a Node.js environment. The application runs in an environment that is similar to Service Worker. If you use Webpack 5, you need to manually add the built-in polyfills of Node.js to the browser environment.

{
  ...
  resolve: {
      fallback: {
      assert: require.resolve('assert/'),
      buffer: require.resolve('buffer/'),
      crypto: require.resolve('crypto-browserify'),
      os: require.resolve('os-browserify/browser'),
      stream: require.resolve('stream-browserify'),
      zlib: require.resolve('browserify-zlib'),
      util: require.resolve('util/'),
    },
    ...
  }
  ...
}

You also need to manually install the following polyfill packages: assert, buffer, crypto-browserify, os-browserify, stream-browserify, browserify-zlib, and util.

Step 5: Add caches

To execute the fetchWeatherOfCity() method, add caches by using the experimental API.

export async function fetchWeatherOfCity(city: string) {
  const url = new URL('http://www.tianqiapi.com/free/day');
  url.searchParams.set('appid', '2303****');
  url.searchParams.set('appsecret', '8Yvl****');
  url.searchParams.set('city', city);
  const urlString = url.toString();
  if (isCacheSupported()) {
    const cachedResponse = await cache.get(urlString);
    if (cachedResponse) {
      return cachedResponse;
    }
  }
  const response = await fetch(urlString);
  if (isCacheSupported()) {
    cache.put(urlString, response);
  }
  return response;
}

The cache object that is provided in globalThis is a cache that is implemented by Swift. The cache key must be an HTTP request object or an HTTP URL string, and the cache value must be an HTTP response object that can be obtained by using the fetch() method. Applications that use the serverless architecture and run in the EdgeRoutine container are restarted every few minutes or every hour. The global variables are cleared each time the applications are restarted. You can cache the global variables on POPs by using object caching.

Step 6: Add the Playground debugger

To efficiently debug GraphQL, you can add the official Playground debugger. Playground is a single-page application. You can load Playground by using the html-loader of Webpack.

addEventListener('fetch', (event) => {
  const response = handleRequest(event.request);
  if (response) {
    event.respondWith(response);
  }
});
function handleRequest(request: Request): Promise<Response> | void {
  const url = new URL(request.url);
  const path = url.pathname;
  // To facilitate debugging, return the Playground information for all GET requests for /graphql. 
  // Return the specific GraphQL API for POST requests.
  if (request.method === 'GET' && path === '/graphql') {
    return Promise.resolve(new Response(rawPlaygroundHTML, { status: 200, headers: { 'content-type': 'text/html' } }));
  }
}

Access /graphql in the browser and enter a query statement.

query CityWeater($name: String!) {
  versions {
    api
    graphql
  }
  weatherOfCity(name: $name) {
    city {
      id
      name
    }
    code
    updateTime
    localized
    tempOfDay
    tempOfNight
  }
}

Set Variables to {"name": "Hangzhou"} and click the Play button.