Fetching from REST
Using RESTDataSource to fetch data from REST APIs
See the
@apollo/datasource-rest
README for the full details of theRESTDataSource
API.
The RESTDataSource
class simplifies fetching data from REST APIs and helps handle caching, request deduplication, and errors while resolving operations.
For more information about fetching from data sources other than a REST API, see Fetching Data.
Creating subclasses
To get started, install the @apollo/datasource-rest
package:
1npm install @apollo/datasource-rest
Your server should define a separate subclass of RESTDataSource
for each REST API it communicates with. Here's an example of a RESTDataSource
subclass that defines two data-fetching methods, getMovie
and getMostViewedMovies
:
1import { RESTDataSource } from '@apollo/datasource-rest';
2
3class MoviesAPI extends RESTDataSource {
4 override baseURL = 'https://movies-api.example.com/';
5
6 async getMovie(id: string): Promise<Movie> {
7 return this.get<Movie>(`movies/${encodeURIComponent(id)}`);
8 }
9
10 async getMostViewedMovies(limit = '10'): Promise<Movie[]> {
11 const data = await this.get('movies', {
12 params: {
13 per_page: limit.toString(), // all params entries should be strings,
14 order_by: 'most_viewed',
15 },
16 });
17 return data.results;
18 }
19}
You can extend the RESTDataSource
class to implement whatever data-fetching methods your resolvers need. These methods should use the built-in convenience methods (e.g., get
and post
) to perform HTTP requests, helping you add query parameters, parse and cache JSON results, dedupe requests, and handle errors. More complex use cases can use the fetch
method directly. The fetch
method returns both the parsed body and the response object, which provides more flexibility for use cases like reading response headers.
Adding data sources to your server's context function
In the examples below, we use top-level
await
calls to start our server asynchronously. Check out our Getting Started guide to see how we configured our project to support this.
You can add data sources to the context
initialization function, like so:
1interface ContextValue {
2 dataSources: {
3 moviesAPI: MoviesAPI;
4 personalizationAPI: PersonalizationAPI;
5 };
6}
7
8const server = new ApolloServer<ContextValue>({
9 typeDefs,
10 resolvers,
11});
12
13const { url } = await startStandaloneServer(server, {
14 context: async () => {
15 const { cache } = server;
16 return {
17 // We create new instances of our data sources with each request,
18 // passing in our server's cache.
19 dataSources: {
20 moviesAPI: new MoviesAPI({ cache }),
21 personalizationAPI: new PersonalizationAPI({ cache }),
22 },
23 };
24 },
25});
26
27console.log(`🚀 Server ready at ${url}`);
Apollo Server calls the context
initialization function for every incoming operation. This means:
For every operation,
context
returns an object containing new instances of yourRESTDataSource
subclasses (in this case,MoviesAPI
andPersonalizationAPI
).The
context
function should create a new instance of eachRESTDataSource
subclass for each operation. More details on why below.
Your resolvers can then access your data sources from the shared contextValue
object and use them to fetch data:
1const resolvers = {
2 Query: {
3 movie: async (_, { id }, { dataSources }) => {
4 return dataSources.moviesAPI.getMovie(id);
5 },
6 mostViewedMovies: async (_, __, { dataSources }) => {
7 return dataSources.moviesAPI.getMostViewedMovies();
8 },
9 favorites: async (_, __, { dataSources }) => {
10 return dataSources.personalizationAPI.getFavorites();
11 },
12 },
13};
Caching
The RESTDataSource
class provides its subclasses with two layers of caching:
The first layer deduplicates concurrent outgoing
GET
(andHEAD
) requests by default. Deduplication is keyed on the request's method and URL. You can configure this behavior by overriding therequestDeduplicationPolicyFor
method. For more details, see the README.
Note: In versions of
RESTDataSource
prior to v5, all outgoingGET
requests are deduplicated. You can achieve this same behavior with thededuplicate-until-invalidated
policy (explained further in the README).
The second layer caches the results from HTTP responses that specify HTTP caching headers.
These caching layers effectively make the RESTDataSource
class a Node HTTP client that offers browser-style caching. Below, we'll dive into each layer of caching and the advantage that layer provides.
GET
(and HEAD
) requests and responses
Every time you instantiate a RESTDataSource
subclass, under the hood that instance creates an internal cache. By default, RESTDataSource
automatically deduplicates concurrent GET
(and HEAD
) requests (keyed by their method and URLs) alongside their results in this internal cache. This behavior is called request deduplication. You can configure this default behavior by overriding the requestDeduplicationPolicyFor
method on the class.
The
RESTDataSource
class cachesGET
(andHEAD
) requests and responses regardless of HTTP caching headers.
The request deduplication cache enables RESTDataSource
to optimize the current operation by eliminating redundant GET
(and HEAD
) requests from different resolvers trying to get the same information. This works much like DataLoader
's caching functionality.
As an example, let's say we have two RESTDataSource
subclasses for fetching data from a Posts API and an Authors API. We can write a query fetching a post's content and that post's author's name:
1query GetPosts {
2 posts {
3 body
4 author {
5 name
6 }
7 }
8}
The above query provides an example of the classic N+1 problem. For every N
number of posts, we'd supposedly make one more request to find the post's author's name (from an endpoint such as /authors/id_1
).
This is a situation where RESTDataSource
can optimize an operation using its cache of memoized GET
requests and their responses.
The first time RESTDataSource
makes a GET
request (e.g., to /authors/id_1
), it stores the request's URL before making that request. RESTDataSource
then performs the request and stores the result alongside the request's URL in its memoized cache forever.
If any resolver in the current operation attempts a parallel GET
request to the same URL, RESTDataSource
checks its memoized cache before performing that request. If a request or a result exists in the cache, RESTDataSource
returns (or waits to return) that stored result without making another request.
This internal caching mechanism is why we create a new
RESTDataSource
instance for every request. Otherwise, responses would be cached across requests even if they specify they shouldn't be!
You can change how GET
(and HEAD
) requests are stored in RESTDataSource
's deduplication cache by overwriting the cacheKeyFor
method. By default, a request's cache key is the combination of its HTTP method and URL.
To restore the deduplication policy from before RESTDataSource
v5, you can configure requestDeduplicationPolicyFor
like so:
1class MoviesAPI extends RESTDataSource {
2 override baseURL = 'https://movies-api.example.com/';
3 private token: string;
4
5 constructor(options: { token: string; cache: KeyValueCache }) {
6 super(options); // this sends our server's `cache` through
7 this.token = options.token;
8 }
9
10 protected override requestDeduplicationPolicyFor(
11 url: URL,
12 request: RequestOptions,
13 ) {
14 const cacheKey = this.cacheKeyFor(url, request);
15 return {
16 policy: 'deduplicate-until-invalidated',
17 deduplicationKey: `${request.method ?? 'GET'} ${cacheKey}`
18 };
19 }
20
21 // Duplicate requests are cached indefinitely
22 async getMovie(id) {
23 return this.get(`movies/${encodeURIComponent(id)}`);
24 }
25}
To disable request deduplication entirely, you can configure requestDeduplicationPolicyFor
like so:
1class MoviesAPI extends RESTDataSource {
2 override baseURL = 'https://movies-api.example.com/';
3 private token: string;
4
5 constructor(options: { token: string; cache: KeyValueCache }) {
6 super(options); // this sends our server's `cache` through
7 this.token = options.token;
8 }
9
10 protected override requestDeduplicationPolicyFor(
11 url: URL,
12 request: RequestOptions,
13 ) {
14 const cacheKey = this.cacheKeyFor(url, request);
15 return { policy: 'do-not-deduplicate' } as const;
16 }
17
18 // Outgoing requests aren't cached, but the HTTP response cache still works!
19 async getMovie(id) {
20 return this.get(`movies/${encodeURIComponent(id)}`);
21 }
22}
Specifying cache TTL
📣 New in Apollo Server 4: Apollo Server no longer automatically provides its cache to data sources. See here for more details.
The RESTDataSource
class can cache results from the REST API it fetches from if either of the following is true:
The request method is
GET
(orHEAD
), and the response specifies caching headers (e.g.,cache-control
).The
RESTDataSource
instance'scacheOptions
specify a TTL.You can do this by overriding the
cacheOptionsFor
method or in the HTTP method making the request.
RESTDataSource
ensures that the cached information honors the TTL (Time To Live) rules established by those caching headers.
Each RESTDataSource
subclass accepts a cache
argument where you can specify which cache to use (e.g., Apollo Server's default cache) to store the results of past fetches:
1// KeyValueCache is the type of Apollo server's default cache
2import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
3
4class PersonalizationAPI extends RESTDataSource {
5 override baseURL = 'https://person.example.com/';
6 private token: string;
7
8 constructor(options: { cache: KeyValueCache; token: string }) {
9 super(options); // this sends our server's `cache` through
10 this.token = options.token;
11 }
12}
13
14// server set up, etc.
15
16const { url } = await startStandaloneServer(server, {
17 context: async ({ req }) => {
18 const token = getTokenFromRequest(req);
19 // We'll take Apollo Server's cache
20 // and pass it to each of our data sources
21 const { cache } = server;
22 return {
23 dataSources: {
24 moviesAPI: new MoviesAPI({ cache, token }),
25 personalizationAPI: new PersonalizationAPI({ cache }),
26 },
27 };
28 },
29});
Passing the same cache
to multiple RESTDataSource
subclass instances enables those instances to share the cached results.
When running multiple instances of your server, you should use an external shared cache backend. This enables one server instance to use the cached result from another instance.
If you want to configure or replace Apollo Server's default cache, see Configuring external caching for more details.
HTTP Methods
RESTDataSource
includes convenience methods for common REST API request methods: get
, post
, put
, patch
, and delete
(see the source).
An example of each is shown below:
Click to expand
1class MoviesAPI extends RESTDataSource {
2 override baseURL = 'https://movies-api.example.com/';
3
4 // GET
5 async getMovie(id) {
6 return this.get(
7 `movies/${encodeURIComponent(id)}`, // path
8 );
9 }
10
11 // POST
12 async postMovie(movie) {
13 return this.post(
14 `movies`, // path
15 { body: { movie } }, // request body
16 );
17 }
18
19 // PUT
20 async newMovie(movie) {
21 return this.put(
22 `movies`, // path
23 { body: { movie } }, // request body
24 );
25 }
26
27 // PATCH
28 async updateMovie(movie) {
29 return this.patch(
30 `movies`, // path
31 { body: { id: movie.id, movie } }, // request body
32 );
33 }
34
35 // DELETE
36 async deleteMovie(movie) {
37 return this.delete(
38 `movies/${encodeURIComponent(movie.id)}`, // path
39 );
40 }
41}
Note the use of encodeURIComponent
in the above snippet. This is a standard function that encodes special characters in a URI, preventing a possible injection attack vector.
For a simple example, suppose our REST endpoint responded to the following URLs:
DELETE
/movies/:id
DELETE
/movies/:id/characters
A "malicious" client could provide an :id
of 1/characters
to target the delete characters
endpoint when it was the singular movie
endpoint that we were trying to delete. URI encoding prevents this kind of injection by transforming the /
into %2F
. This can then be correctly decoded and interpreted by the server and won't be treated as a path segment.
Method parameters
For all HTTP convenience methods, the first parameter is the relative path of the endpoint you're sending the request to (e.g., movies
). The second parameter is an object where you can set a request's headers
, params
, cacheOptions
, and body
:
1class MoviesAPI extends RESTDataSource {
2 override baseURL = 'https://movies-api.example.com/';
3
4 // an example making an HTTP POST request
5 async postMovie(movie) {
6 return this.post(
7 `movies`, // path
8 { body: movie }, // request body
9 );
10 }
11}
Setting fetch options
The second parameter passed to each REST method is an object containing request options. These include options commonly passed to fetch
, including method
, headers
, body
, and signal
.
If you're looking for other advanced options that aren't covered in the Apollo docs, you can set them here and refer to your Fetch API docs.
1this.get('/movies/1', options);
Setting timeouts
To set a fetch
timeout, provide an AbortSignal
via the signal
option, which enables you to abort the request with custom logic.
Here's an example of a simple timeout after a fixed time for every request:
1this.get('/movies/1', { signal: AbortSignal.timeout(myTimeoutMilliseconds) });
Intercepting fetches
New in Apollo Server 4: Apollo Server 4 now uses the
@apollo/utils.fetcher
interface under the hood for fetching. This interface lets you choose your own implementation of the Fetch API. To ensure compatibility with all Fetch implementations, the request provided to hooks likewillSendRequest
is a plain JS object rather than aRequest
object with methods.
RESTDataSource
includes a willSendRequest
method that you can override to modify outgoing requests before they're sent. For example, you can use this method to add headers or query parameters. This method is most commonly used for authorization or other concerns that apply to all sent requests.
Data sources also have access to the GraphQL operation context, which is useful for storing a user token or other relevant information.
If you're using TypeScript, make sure to import the
AugmentedRequest
type.
Setting a header
1import {
2 RESTDataSource,
3 AugmentedRequest,
4} from '@apollo/datasource-rest';
5import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
6
7class PersonalizationAPI extends RESTDataSource {
8 override baseURL = 'https://movies-api.example.com/';
9 private token: string;
10
11 constructor(options: { token: string; cache: KeyValueCache }) {
12 super(options);
13 this.token = options.token;
14 }
15
16 override willSendRequest(_path: string, request: AugmentedRequest) {
17 request.headers['authorization'] = this.token;
18 }
19}
Adding a query parameter
1import {
2 RESTDataSource,
3 AugmentedRequest,
4} from '@apollo/datasource-rest';
5import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
6
7class PersonalizationAPI extends RESTDataSource {
8 override baseURL = 'https://movies-api.example.com/';
9 private token: string;
10
11 constructor(options: { token: string; cache: KeyValueCache }) {
12 super(options);
13 this.token = options.token;
14 }
15
16 override willSendRequest(_path: string, request: AugmentedRequest) {
17 request.params.set('api_key', this.token);
18 }
19}
Resolving URLs dynamically
In some cases, you'll want to set the URL based on the environment or other contextual values. To do this, you can override resolveURL
:
1import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';
2import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
3
4class PersonalizationAPI extends RESTDataSource {
5 private token: string;
6
7 constructor(options: { token: string; cache: KeyValueCache }) {
8 super(options);
9 this.token = options.token;
10 }
11
12 override async resolveURL(path: string, request: AugmentedRequest) {
13 if (!this.baseURL) {
14 const addresses = await resolveSrv(
15 path.split('/')[1] + '.service.consul',
16 );
17 this.baseURL = addresses[0];
18 }
19 return super.resolveURL(path, request);
20 }
21}
Using with DataLoader
The DataLoader utility was designed for a specific use case: deduplicating and batching object loads from a data store. It provides a memoization cache, which avoids loading the same object multiple times during a single GraphQL request. It also combines loads that occur during a single tick of the event loop into a batched request that fetches multiple objects at once.
DataLoader is great for its intended use case, but it’s less helpful when loading data from REST APIs. This is because its primary feature is batching, not caching.
When layering GraphQL over REST APIs, it's most helpful to have a resource cache that:
Saves data across multiple GraphQL requests
Can be shared across multiple GraphQL servers
Provides cache management features like expiry and invalidation that use standard HTTP cache control headers
Batching with REST APIs
Most REST APIs don't support batching. When they do, using a batched endpoint can jeopardize caching. When you fetch data in a batch request, the response you receive is for the exact combination of resources you're requesting. Unless you request that same combination again, future requests for the same resource won't be served from cache.
We recommend that you restrict batching to requests that can't be cached. In these cases, you can take advantage of DataLoader as a private implementation detail inside your RESTDataSource
:
1import DataLoader from 'dataloader';
2import {
3 RESTDataSource,
4 AugmentedRequest,
5} from '@apollo/datasource-rest';
6import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
7
8class PersonalizationAPI extends RESTDataSource {
9 override baseURL = 'https://movies-api.example.com/';
10 private token: string;
11
12 constructor(options: { token: string; cache: KeyValueCache }) {
13 super(options); // this should send our server's `cache` through
14 this.token = options.token;
15 }
16
17 override willSendRequest(_path: string, request: AugmentedRequest) {
18 request.headers['authorization'] = this.token;
19 }
20
21 private progressLoader = new DataLoader(async (ids) => {
22 const progressList = await this.get('progress', {
23 params: { ids: ids.join(',') },
24 });
25 return ids.map((id) => progressList.find((progress) => progress.id === id));
26 });
27
28 async getProgressFor(id) {
29 return this.progressLoader.load(id);
30 }
31}