GraphQL Summit is back for three days of insights, hands-on learning, and fun to celebrate the GraphQL community. Join us in San Diego Oct 3-5.
Docs
Try Apollo Studio

Fetching from REST

Using RESTDataSource to fetch data from REST APIs


See the @apollo/datasource-rest page for the full details of the RESTDataSource API.

The RESTDataSource class helps you fetch data from REST APIs. The RESTDataSource class helps handle caching, deduplication, and errors while resolving operations.

ApolloServer
Fetches data
Sends query
RESTDataSource
REST API
ApolloClient

Creating subclasses

To get started, install the @apollo/datasource-rest package:

npm 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:

movies-api.ts
import { RESTDataSource } from '@apollo/datasource-rest';
class MoviesAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
async getMovie(id): 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,
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,
order_by: 'most_viewed',
},
});
return data.results;
}
}

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.

Adding data sources to Apollo Server's context

You can add data sources to the context initialization function, like so:

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}`);

Apollo Server calls the context initialization function for every incoming operation. This means:

  • For every operation, context returns an object containing new instances of your RESTDataSource subclasses (in this case, MoviesAPI and PersonalizationAPI).
  • The context function should create a new instance of each RESTDataSource subclass for each operation.

Your resolvers can then access your data sources from the shared context object and use them to fetch data:

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();
},
},
};

Caching

📣 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 if the REST API it fetches from specifies caching headers in its HTTP responses (e.g., cache-control).

As shown in the above code snippet, by default, each RESTDataSource subclass accepts a cache argument (e.g., Apollo Server's default cache) to store the results of past fetches:

class MoviesAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
// We omit the constructor function here because
// RESTDataSource accepts a cache argument by default
}
// server set up, etc.
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const { cache } = server;
return {
dataSources: {
moviesAPI: new MoviesAPI({ cache }),
personalizationAPI: new PersonalizationAPI({ cache }),
},
};
},
});

If your RESTDataSource subclass accepts multiple arguments, make sure you add a constructor function, like so:

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { RESTDataSource } from '@apollo/datasource-rest';
// KeyValueCache is the type of Apollo server's default cache
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 sends our server's `cache` through
this.token = options.token;
}
// data fetching methods, etc.
}
// set up server, context typing, etc.
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = getTokenFromRequest(req);
const { cache } = server;
return {
dataSources: {
personalizationApi: new PersonalizationAPI({ cache, token }),
},
};
},
});
import { startStandaloneServer } from '@apollo/server/standalone';
import { RESTDataSource } from '@apollo/datasource-rest';
// KeyValueCache is the type of Apollo server's default cache
class PersonalizationAPI extends RESTDataSource {
baseURL = 'https://movies-api.example.com/';
constructor(options) {
super(options); // this sends our server's `cache` through
this.token = options.token;
}
// data fetching methods, etc.
}
// set up server, context typing, etc.
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = getTokenFromRequest(req);
const { cache } = server;
return {
dataSources: {
personalizationApi: new PersonalizationAPI({ cache, token }),
},
};
},
});

When running multiple instances of your server, you should use a 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:

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:

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
);
}
}

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 like willSendRequest is a plain JS object rather than a Request 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 WillSendRequestOptions type.

Setting a header

import { RESTDataSource, WillSendRequestOptions } 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(request: WillSendRequestOptions) {
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(request) {
request.headers['authorization'] = this.token;
}
}

Adding a query parameter

import { RESTDataSource, WillSendRequestOptions } 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(request: WillSendRequestOptions) {
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(request) {
request.params.set('api_key', this.token);
}
}

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:

import { RESTDataSource, RequestOptions } 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: RequestOptions) {
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);
}
}

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:

import DataLoader from 'dataloader';
import { RESTDataSource, WillSendRequestOptions } 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(request: WillSendRequestOptions) {
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);
}
}
Edit on GitHub
Previous
Resolvers
Next
Error handling