加入我们,于10月8日至10日在纽约市学习最新的GraphQL Federation和API平台工程的技巧、趋势和新闻。加入我们,参加2024年纽约市的GraphQL高峰会
文档
免费开始

从 REST 获取

使用RESTDataSource从REST API获取数据


参见@apollo/datasource-rest README获得关于RESTDataSource API的完整信息。

RESTDataSource 简化了从REST API获取数据的过程,并有助于处理缓存、请求去重和错误,同时解决

ApolloServer
Fetches data
Fetches data
Sends query
MoviesAPI extends RESTDataSource
BooksAPI extends RESTDataSource
Books REST API
Movies REST API
ApolloClient

有关从除了REST API之外的数据源获取更多信息的说明,请参见获取数据

创建子类

要开始使用,请安装@apollo/datasource-rest:

npm install @apollo/datasource-rest

您的服务器应为与每个REST API通信的RESTDataSource定义一个单独的子类。以下是一个RESTDataSource子类的示例,它定义了两个数据检索方法,getMovie和getMostViewedMovies:RESTDataSource

movies-api.ts
import { RESTDataSource } from '@apollo/datasource-rest';
class MoviesAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
async getMovie(id: string): Promise<Movie> {
return this.get<Movie>(`movies/${encodeURIComponent(id)}`);
}
async getMostViewedMovies(limit = '10'): Promise<Movie[]> {
const data = await this.get('movies', {
params: {
per_page: limit.toString(), // all params entries should be strings,
order_by: 'most_viewed',
},
});
return data.results;
}
}
movies-api.js
import { RESTDataSource } from '@apollo/datasource-rest';
class MoviesAPI extends RESTDataSource {
baseURL = 'https://movies-api.example.com/';
async getMovie(id) {
return this.get(`movies/${encodeURIComponent(id)}`);
}
async getMostViewedMovies(limit = '10') {
const data = await this.get('movies', {
params: {
per_page: limit.toString(), // all params entries should be strings,
order_by: 'most_viewed',
},
});
return data.results;
}
}

您可以通过扩展RESTDataSource类来实施所需的数据检索方法。这些方法应使用内置的便捷方法(例如 get 和 post)进行HTTP请求,帮助您添加查询参数,解析和缓存JSON结果,去重请求和处理错误。更复杂的场景可以直接使用 fetch 方法。fetch 方法返回解析后的主体和响应对象,为读取响应头等使用场景提供了更多灵活性。

向服务器上下文函数添加数据源

注意

以下示例中,我们使用顶层await调用异步启动服务器。如果您想了解如何设置,请查看入门指南的详细说明。

您可以将数据源添加到上下文初始化函数中,如下所示:

index.ts
interface ContextValue {
dataSources: {
moviesAPI: MoviesAPI;
personalizationAPI: PersonalizationAPI;
};
}
const server = new ApolloServer<ContextValue>({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async () => {
const { cache } = server;
return {
// We create new instances of our data sources with each request,
// passing in our server's cache.
dataSources: {
moviesAPI: new MoviesAPI({ cache }),
personalizationAPI: new PersonalizationAPI({ cache }),
},
};
},
});
console.log(`🚀 Server ready at ${url}`);
index.js
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async () => {
const { cache } = server;
return {
// We create new instances of our data sources with each request,
// passing in our server's cache.
dataSources: {
moviesAPI: new MoviesAPI({ cache }),
personalizationAPI: new PersonalizationAPI({ cache }),
},
};
},
});
console.log(`🚀 Server ready at ${url}`);

调用上下文初始化函数用于每个传入操作这意味着:

  • 对于每个上下文返回一个对象,其中包含您RESTDataSource子类的新实例(在本例中为MoviesAPIPersonalizationAPI)。
  • 每个上下文函数应该为每个操作创建每个RESTDataSource子类的一个新实例。有关更多信息,请参见以下内容。

您的解析器可以从共享的上下文值对象中访问您的数据源,并使用它们来获取数据:

resolvers.ts
const resolvers = {
Query: {
movie: async (_, { id }, { dataSources }) => {
return dataSources.moviesAPI.getMovie(id);
},
mostViewedMovies: async (_, __, { dataSources }) => {
return dataSources.moviesAPI.getMostViewedMovies();
},
favorites: async (_, __, { dataSources }) => {
return dataSources.personalizationAPI.getFavorites();
},
},
};

缓存

RESTDataSource类为它的子类提供了两层的缓存:

  • 第一层按默认方式去重并发发出的GET(和HEAD)请求。去重键是请求的方法和URL。您可以通过覆盖requestDeduplicationPolicyFor方法来配置此行为。有关更多详情,请参阅README

注意:在版本低于RESTDataSource 5之前的版本中,所有发出的GET请求都会被去重。您可以使用deduplicate-until-invalidated策略(在README中进一步解释)来实现相同的行为。

  • 第二层缓存指定了HTTP缓存头的HTTP响应结果。

这些缓存层使RESTDataSource类成为一个提供浏览器样式缓存的Node HTTP客户端。以下,我们将深入了解每一层的缓存及其提供的优势。

GET(和HEAD)请求和响应

query GetPosts {
posts {
body
author {
name
}
}
}

第一次RESTDataSource发起GET请求(例如/authors/id_1),它会先将请求的URL存储在发送请求之前。接着RESTDataSource执行请求,并将结果永远存储在与其请求URL相关的缓存中。

如果当前操作中的任何尝试对相同URL进行并行GET请求,RESTDataSource将检查其缓存在执行此请求之前。如果缓存中存在请求或结果,RESTDataSource会返回(或等待返回)存储的结果而不执行另一个请求。

这种内部缓存机制是我们为每个请求创建新的RESTDataSource实例的原因。否则,即使请求指定它们不应被缓存,响应也会在请求之间进行缓存!

您可以通过覆盖RESTDataSourcecacheKeyFor方法来更改GET(和HEAD)请求在RESTDataSource的去除重复缓存中的存储方式。

要从RESTDataSource v5之前恢复去重策略,您可以按以下方式配置requestDeduplicationPolicyFor

class MoviesAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
private token: string;
constructor(options: { token: string; cache: KeyValueCache }) {
super(options); // this sends our server's `cache` through
this.token = options.token;
}
protected override requestDeduplicationPolicyFor(url: URL, request: RequestOptions) {
const cacheKey = this.cacheKeyFor(url, request);
return {
policy: 'deduplicate-until-invalidated',
deduplicationKey: `${request.method ?? 'GET'} ${cacheKey}`,
};
}
// Duplicate requests are cached indefinitely
async getMovie(id) {
return this.get(`movies/${encodeURIComponent(id)}`);
}
}
class MoviesAPI extends RESTDataSource {
baseURL = 'https://movies-api.example.com/';
constructor(options) {
super(options); // this sends our server's `cache` through
this.token = options.token;
}
requestDeduplicationPolicyFor(url, request) {
const cacheKey = this.cacheKeyFor(url, request);
return {
policy: 'deduplicate-until-invalidated',
deduplicationKey: `${request.method ?? 'GET'} ${cacheKey}`,
};
}
// Duplicate requests are cached indefinitely
async getMovie(id) {
return this.get(`movies/${encodeURIComponent(id)}`);
}
}

要完全禁用请求去重,您可以按以下方式配置requestDeduplicationPolicyFor

class MoviesAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
private token: string;
constructor(options: { token: string; cache: KeyValueCache }) {
super(options); // this sends our server's `cache` through
this.token = options.token;
}
protected override requestDeduplicationPolicyFor(url: URL, request: RequestOptions) {
const cacheKey = this.cacheKeyFor(url, request);
return { policy: 'do-not-deduplicate' } as const;
}
// Outgoing requests aren't cached, but the HTTP response cache still works!
async getMovie(id) {
return this.get(`movies/${encodeURIComponent(id)}`);
}
}
class MoviesAPI extends RESTDataSource {
baseURL = 'https://movies-api.example.com/';
constructor(options) {
super(options); // this sends our server's `cache` through
this.token = options.token;
}
requestDeduplicationPolicyFor(url, request) {
const cacheKey = this.cacheKeyFor(url, request);
return { policy: 'do-not-deduplicate' };
}
// Outgoing requests aren't cached, but the HTTP response cache still works!
async getMovie(id) {
return this.get(`movies/${encodeURIComponent(id)}`);
}
}

指定缓存TTL

📣 新功能!Apollo Server 4中:Apollo Server不再自动为其数据源提供缓存。了解更多详细信息

如果以下任一条件为真,RESTDataSource类可以缓存从REST API获取的结果:

  • 请求方法为GET(或HEAD),并且响应指定了缓存头(例如,cache-control)。
  • RESTDataSource实例的缓存选项指定一个TTL。
    • 您可以这样做,通过覆盖方法,或者在发起请求的HTTP方法中。

RESTDataSource确保缓存的信息遵守由这些缓存头指定的TTL(生命周期)规则。

每个RESTDataSource子类接受一个缓存参数,您可以在其中指定要使用的缓存(例如:Apollo Server的默认缓存)以存储过去的fetch结果:

// KeyValueCache is the type of Apollo server's default cache
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
class PersonalizationAPI extends RESTDataSource {
override baseURL = 'https://person.example.com/';
private token: string;
constructor(options: { cache: KeyValueCache; token: string }) {
super(options); // this sends our server's `cache` through
this.token = options.token;
}
}
// server set up, etc.
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = getTokenFromRequest(req);
// We'll take Apollo Server's cache
// and pass it to each of our data sources
const { cache } = server;
return {
dataSources: {
moviesAPI: new MoviesAPI({ cache, token }),
personalizationAPI: new PersonalizationAPI({ cache }),
},
};
},
});
// KeyValueCache is the type of Apollo server's default cache
class PersonalizationAPI extends RESTDataSource {
baseURL = 'https://person.example.com/';
constructor(options) {
super(options); // this sends our server's `cache` through
this.token = options.token;
}
}
// server set up, etc.
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = getTokenFromRequest(req);
// We'll take Apollo Server's cache
// and pass it to each of our data sources
const { cache } = server;
return {
dataSources: {
moviesAPI: new MoviesAPI({ cache, token }),
personalizationAPI: new PersonalizationAPI({ cache }),
},
};
},
});
export {};

将相同的缓存传递给多个RESTDataSource子类实例,启用这些实例可共享缓存结果。

当运行您的服务器多个实例时,您应该使用一个外部共享缓存后端。这样,一个服务器实例可以使用来自另一个实例的缓存结果。

如果您想配置或替换Apollo Server的默认缓存,请参见配置外部缓存以获取更多详情。

HTTP方法

RESTDataSource包括常用REST API请求方法的便捷方法:getpostputpatchdelete(详情见源代码)。

下面是每个方法的示例

注意上述代码中 encodeURIComponent 的使用。这是一个标准的函数,用于在 URI 中对特殊字符进行编码,从而防止可能的注入攻击向量。

以下是一个简单的例子,假设我们的 REST 端点 responded 到以下 URL

  • DELETE /movies/:id
  • DELETE /movies/:id/characters

一个 "恶意" 的客户端可以提供一个 :id1/characters 的值,以针对 characters 删除端点(当我们试图删除单个 movie 端点)。URI 编码通过将 / 转换为 %2F 来防止这种注入。这样,它可以由服务器正确解码和理解,不会被视为路径段。

方法参数

对于所有 HTTP 方便方法,第一个参数是要发送请求的端点的相对路径(例如,movies)。第二个参数是一个对象,您可以在其中设置请求的 headersparamscacheOptionsbody

class MoviesAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
// an example making an HTTP POST request
async postMovie(movie) {
return this.post(
`movies`, // path
{ body: movie }, // request body
);
}
}

设置 fetch 选项

每个 REST 方法传入的第二个参数是一个包含请求选项的对象。这些选项包括通常传递给 fetch 的选项,包括 methodheadersbodysignal

如果您正在寻找 Apollo 文档中没有涉及的其它高级选项,您可以将它们设置在这里并参考您的 Fetch API 文档。

this.get('/movies/1', options);

设置超时

要设置 fetch 超时,请通过 AbortSignal 选项提供一个 AbortSignal,这允许您使用自定义逻辑终止请求。

以下是一个示例,显示每个请求后的简单超时设置

this.get('/movies/1', { signal: AbortSignal.timeout(myTimeoutMilliseconds) });

拦截 fetch 请求

Apollo Server 4 的新特性Apollo Server 4 现在底层使用 @apollo/utils.fetcher 界面来进行获取。此界面允许您选择自己的 Fetch API 实现。为了确保与所有 Fetch 实现兼容,提供给钩子(如 willSendRequest)的请求是一个普通的 JS 对象,不是一个具有方法的 Request 对象。

RESTDataSource 包含一个 willSendRequest 方法,您可以使用该方法覆盖以修改在发送之前的外出请求。例如,您可以使用此方法添加头信息或查询参数。此方法通常用于授权或适用于所有发送请求的其他关注点。

还可以访问 GraphQL 操作上下文,这对于存储用户令牌或其他相关信息非常有用。

如果您正在使用 TypeScript,请确保导入 AugmentedRequest 类型。

设置头信息

import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
class PersonalizationAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
private token: string;
constructor(options: { token: string; cache: KeyValueCache }) {
super(options);
this.token = options.token;
}
override willSendRequest(_path: string, request: AugmentedRequest) {
request.headers['authorization'] = this.token;
}
}
import { RESTDataSource } from '@apollo/datasource-rest';
class PersonalizationAPI extends RESTDataSource {
baseURL = 'https://movies-api.example.com/';
constructor(options) {
super(options);
this.token = options.token;
}
willSendRequest(_path, request) {
request.headers['authorization'] = this.token;
}
}

添加查询参数

import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
class PersonalizationAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
private token: string;
constructor(options: { token: string; cache: KeyValueCache }) {
super(options);
this.token = options.token;
}
override willSendRequest(_path: string, request: AugmentedRequest) {
request.params.set('api_key', this.token);
}
}
import { RESTDataSource } from '@apollo/datasource-rest';
class PersonalizationAPI extends RESTDataSource {
baseURL = 'https://movies-api.example.com/';
constructor(options) {
super(options);
this.token = options.token;
}
willSendRequest(_path, request) {
request.params.set('api_key', this.token);
}
}

动态解析 URL

在某些情况下,您可能希望根据环境或其他上下文值设置 URL。为此,您可以使用 resolveURL 进行覆盖:

import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
class PersonalizationAPI extends RESTDataSource {
private token: string;
constructor(options: { token: string; cache: KeyValueCache }) {
super(options);
this.token = options.token;
}
override async resolveURL(path: string, request: AugmentedRequest) {
if (!this.baseURL) {
const addresses = await resolveSrv(path.split('/')[1] + '.service.consul');
this.baseURL = addresses[0];
}
return super.resolveURL(path, request);
}
}
import { RESTDataSource } from '@apollo/datasource-rest';
class PersonalizationAPI extends RESTDataSource {
constructor(options) {
super(options);
this.token = options.token;
}
async resolveURL(path, request) {
if (!this.baseURL) {
const addresses = await resolveSrv(path.split('/')[1] + '.service.consul');
this.baseURL = addresses[0];
}
return super.resolveURL(path, request);
}
}

与 DataLoader 一起使用

DataLoader 工具的 DataLoader 被设计用于特定的用例:从数据存储中去重和分批加载对象。它提供了缓存记忆功能,可以在单个 GraphQL 请求期间避免加载相同的对象多次。它还合并了在单个事件循环周期中发生加载,形成一个同时获取多个对象的批处理请求。

DataLoader 对于其预期用途非常有用,但在从 REST API 加载数据时不太有帮助。这是因为其主要功能是 批处理,而不是 缓存

在 GraphQL 上层 REST API 时,最具帮助的是具有以下功能的资源缓存:

  • 跨多个 GraphQL 请求保存数据
  • 可以跨多个 共享
  • 提供使用标准 HTTP 缓存控制头进行有效期和失效管理的缓存管理功能

使用 REST API 进行批处理

大多数 REST API 不支持批处理。当它们支持时,使用批处理端点可能会 危害 缓存。当您在批处理请求中获取数据时,收到的响应是对您请求的资源组合的精确响应。除非您再次请求相同的组合,否则未来的同资源请求不会从缓存中提供。

我们建议将批处理限制为不能缓存请求。在这些情况下,您可以在 RESTDataSource 内部作为私有实现细节使用 DataLoader:

import DataLoader from 'dataloader';
import {
RESTDataSource,
AugmentedRequest,
} from '@apollo/datasource-rest';
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
class PersonalizationAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
private token: string;
constructor(options: { token: string; cache: KeyValueCache }) {
super(options); // this should send our server's `cache` through
this.token = options.token;
}
override willSendRequest(_path: string, request: AugmentedRequest) {
request.headers['authorization'] = this.token;
}
private progressLoader = new DataLoader(async (ids) => {
const progressList = await this.get('progress', {
params: { ids: ids.join(',') },
});
return ids.map((id) => progressList.find((progress) => progress.id === id));
});
async getProgressFor(id) {
return this.progressLoader.load(id);
}
}
Previous
概述
Next
集成
评分文章评分在GitHub上编辑编辑论坛Discord

©2024Apollo Graph Inc.,即Apollo GraphQL。

隐私政策

公司