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.
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
:
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;}}
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:
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}`);
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 yourRESTDataSource
subclasses (in this case,MoviesAPI
andPersonalizationAPI
). - The
context
function should create a new instance of eachRESTDataSource
subclass for each operation.
Your resolvers can then access your data sources from the shared context
object and use them to fetch data:
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 cacheimport 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` throughthis.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 cacheclass PersonalizationAPI extends RESTDataSource {baseURL = 'https://movies-api.example.com/';constructor(options) {super(options); // this sends our server's `cache` throughthis.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 requestasync 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` throughthis.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);}}