Apollo Server 3 is officially end-of-life as of 22 October 2024.

Learn more about upgrading.

Data sources

Manage connections to databases and REST APIs


Data sources are classes that Apollo Server can use to encapsulate fetching data from a particular source, such as a database or a REST API. These classes help handle caching, deduplication, and errors while resolving operations.

Your server can use any number of different data sources. You don't have to use data sources to fetch data, but they're strongly recommended.

Open-source implementations

All data source implementations extend the generic DataSource abstract class, which is included in the apollo-datasource package. Subclasses of a DataSource should define whatever logic is required to communicate with a particular store or API.

Apollo and the larger community maintain the following open-source implementations:

Do you maintain a DataSource implementation that isn't listed here? Please submit a PR to be added to the list!

ClassSourceFor Use With
RESTDataSourceApolloREST APIs (see below)
HTTPDataSourceCommunityHTTP/REST APIs (newer community alternative to RESTDataSource)
SQLDataSourceCommunitySQL databases (via Knex.js)
MongoDataSourceCommunityMongoDB
CosmosDataSourceCommunityAzure Cosmos DB
FirestoreDataSourceCommunityCloud Firestore

If none of these implementations applies to your use case, you can create your own custom DataSource subclass.

Apollo does not provide official support for community-maintained libraries. We cannot guarantee that community-maintained libraries adhere to best practices, or that they will continue to be maintained.

Adding data sources to Apollo Server

You provide your DataSource subclasses to the ApolloServer constructor, like so:

JavaScript
index.js
1const server = new ApolloServer({
2  typeDefs,
3  resolvers,
4  csrfPrevention: true,
5  cache: "bounded",
6  plugins: [
7    ApolloServerPluginLandingPageLocalDefault({ embed: true }),
8  ],
9  dataSources: () => {
10    return {
11      moviesAPI: new MoviesAPI(),
12      personalizationAPI: new PersonalizationAPI(),
13    };
14  },
15});
  • As shown, the dataSources option is a function. This function returns an object containing instances of your DataSource subclasses (in this case, MoviesAPI and PersonalizationAPI).

  • Apollo Server calls this function for every incoming operation. It automatically assigns the returned object to the dataSources field of the context object that's passed between your server's resolvers.

  • Also as shown, the function should create a new instance of each data source for each operation. If multiple operations share a single data source instance, you might accidentally combine results from multiple operations.

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

JavaScript
resolvers.js
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

By default, data source implementations use Apollo Server's in-memory cache to store the results of past fetches.

When you initialize Apollo Server, you can provide its constructor a different cache object that implements the KeyValueCache interface. This enables you to back your cache with shared stores like Memcached or Redis.

JavaScript
server.js
1const server = new ApolloServer({
2  typeDefs,
3  resolvers,
4  cache: new MyCustomKeyValueCache()
5});

Using an external cache backend

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.

Apollo Server supports using Memcached, Redis, or other cache backends via the keyv package. For examples, see Configuring external caching.

You can also choose to implement your own cache backend. For more information, see Implementing your own cache backend.

RESTDataSource

The RESTDataSource abstract class helps you fetch data from REST APIs. Your server defines a separate subclass of RESTDataSource for each REST API it communicates with.

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

Bash
1npm install apollo-datasource-rest

You then extend the RESTDataSource class and implement whatever data-fetching methods your resolvers need. These methods can use built-in convenience methods (like get and post) to perform HTTP requests, helping you add query parameters, parse JSON results, and handle errors.

API Reference

To see the all the properties and functions that can be overridden, the source code is always the best option.

Constructor Parameters

httpFetch

Optional constructor option which allows overriding the fetch implementation used when calling data sources.

Properties

baseURL

Optional value to use for all the REST calls. If it is set in your class implementation, this base URL is used as the prefix for all calls. If it is not set, then the value passed to the REST call is exactly the value used.

JavaScript
baseURL.js
1class MoviesAPI extends RESTDataSource {
2  constructor() {
3    super();
4    this.baseURL = 'https://movies-api.example.com/';
5  }
6
7  // GET
8  async getMovie(id) {
9    return this.get(
10      `movies/${encodeURIComponent(id)}` // path
11    );
12  }
13}
memoizeGetRequests

By default, RESTDataSource caches all outgoing GET requests in a separate memoized cache from the regular response cache. It makes the assumption that all responses from HTTP GET calls are cacheable by their URL. If a request is made with the same cache key (URL by default) but with an HTTP method other than GET, the cached request is then cleared.

If you would like to disable the GET request cache, set the memoizeGetRequests property to false. You might want to do this if your API is not actually cacheable or your data changes over time.

JavaScript
memoizeGetRequests.js
1class MoviesAPI extends RESTDataSource {
2  constructor() {
3    super();
4    // Defaults to true
5    this.memoizeGetRequests = false;
6  }
7
8  // Outgoing requests are never cached, however the response cache is still enabled
9  async getMovie(id) {
10    return this.get(
11      `https://movies-api.example.com/movies/${encodeURIComponent(id)}` // path
12    );
13  }
14}

Methods

cacheKeyFor

By default, RESTDatasource uses the full request URL as the cache key. Override this method to remove query parameters or compute a custom cache key.

For example, you could use this to use header fields as part of the cache key. Even though we do validate header fields and don't serve responses from cache when they don't match, new responses overwrite old ones with different header fields.

willSendRequest

This method is invoked just before the fetch call is made. If a Promise is returned from this method it will wait until the promise is completed to continue executing the request.

cacheOptionsFor

Allows setting the CacheOptions to be used for each request/response in the HTTPCache. This is separate from the request-only cache.

didReceiveResponse

By default, this method checks if the response was returned successfully and parses the response into the result object. If the response had an error, it detects which type of HTTP error and throws the error result.

If you override this behavior, be sure to implement the proper error handling.

didEncounterError

By default, this method just throws the error it was given. If you override this method, you can choose to either perform some additional logic and still throw, or to swallow the error by not throwing the error result.

Example

Here's an example RESTDataSource subclass that defines two data-fetching methods, getMovie and getMostViewedMovies:

JavaScript
movies-api.js
1const { RESTDataSource } = require('apollo-datasource-rest');
2
3class MoviesAPI extends RESTDataSource {
4  constructor() {
5    // Always call super()
6    super();
7    // Sets the base URL for the REST API
8    this.baseURL = 'https://movies-api.example.com/';
9  }
10
11  async getMovie(id) {
12    // Send a GET request to the specified endpoint
13    return this.get(`movies/${encodeURIComponent(id)}`);
14  }
15
16  async getMostViewedMovies(limit = 10) {
17    const data = await this.get('movies', {
18      // Query parameters
19      per_page: limit,
20      order_by: 'most_viewed',
21    });
22    return data.results;
23  }
24}

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
JavaScript
1class MoviesAPI extends RESTDataSource {
2  constructor() {
3    super();
4    this.baseURL = 'https://movies-api.example.com/';
5  }
6
7  // GET
8  async getMovie(id) {
9    return this.get(
10      `movies/${encodeURIComponent(id)}` // path
11    );
12  }
13
14  // POST
15  async postMovie(movie) {
16    return this.post(
17      `movies`, // path
18      movie, // request body
19    );
20  }
21
22  // PUT
23  async newMovie(movie) {
24    return this.put(
25      `movies`, // path
26      movie, // request body
27    );
28  }
29
30  // PATCH
31  async updateMovie(movie) {
32    return this.patch(
33      `movies`, // path
34      { id: movie.id, movie }, // request body
35    );
36  }
37
38  // DELETE
39  async deleteMovie(movie) {
40    return this.delete(
41      `movies/${encodeURIComponent(movie.id)}`, // path
42    );
43  }
44}

Note the use of encodeURIComponent. This is a standard JavaScript 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 depends on the HTTP method:

  • For HTTP methods with a request body (post, put, patch), the second parameter is the request body.

  • For HTTP methods without a request body, the second parameter is an object with keys and values corresponding to the request's query parameters.

For all methods, the third parameter is an init object that enables you to provide additional options (such as headers and referrers) to the fetch API that's used to send the request. For details, see MDN's fetch docs.

Intercepting fetches

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.

Setting a header

JavaScript
1class PersonalizationAPI extends RESTDataSource {
2  willSendRequest(request) {
3    request.headers.set('Authorization', this.context.token);
4  }
5}

Adding a query parameter

JavaScript
1class PersonalizationAPI extends RESTDataSource {
2  willSendRequest(request) {
3    request.params.set('api_key', this.context.token);
4  }
5}

Using with TypeScript

If you're using TypeScript, make sure to import the RequestOptions type:

TypeScript
1import { RESTDataSource, RequestOptions } from 'apollo-datasource-rest';
2
3class PersonalizationAPI extends RESTDataSource {
4  baseURL = 'https://personalization-api.example.com/';
5
6  willSendRequest(request: RequestOptions) {
7    request.headers.set('Authorization', this.context.token);
8  }
9}

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:

JavaScript
1async resolveURL(request: RequestOptions) {
2  if (!this.baseURL) {
3    const addresses = await resolveSrv(request.path.split("/")[1] + ".service.consul");
4    this.baseURL = addresses[0];
5  }
6  return super.resolveURL(request);
7}

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:

JavaScript
1class PersonalizationAPI extends RESTDataSource {
2  constructor() {
3    super();
4    this.baseURL = 'https://personalization-api.example.com/';
5  }
6
7  willSendRequest(request) {
8    request.headers.set('Authorization', this.context.token);
9  }
10
11  private progressLoader = new DataLoader(async (ids) => {
12    const progressList = await this.get('progress', {
13      ids: ids.join(','),
14    });
15    return ids.map(id =>
16      progressList.find((progress) => progress.id === id),
17    );
18  });
19
20  async getProgressFor(id) {
21    return this.progressLoader.load(id);
22  }
Feedback

Edit on GitHub

Forums