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-server
library. Subclasses define whatever logic is required to communicate with a particular store or API.
Apollo and the larger community maintain the following open-source implementatons:
Do you maintain a
DataSource
implementation that isn't listed here? Please submit a PR to be added to the list!
Class | Source | For Use With |
---|---|---|
RESTDataSource | Apollo | REST APIs (see below) |
HTTPDataSource | Community | HTTP/REST APIs (newer community alternative to RESTDataSource ) |
SQLDataSource | Community | SQL databases (via Knex.js) |
MongoDataSource | Community | MongoDB |
CosmosDataSource | Community | Azure Cosmos DB |
FirestoreDataSource | Community | Cloud 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:
1const server = new ApolloServer({
2 typeDefs,
3 resolvers,
4 dataSources: () => {
5 return {
6 moviesAPI: new MoviesAPI(),
7 personalizationAPI: new PersonalizationAPI(),
8 };
9 },
10});
As shown, the
dataSources
option is a function. This function returns an object containing instances of yourDataSource
subclasses (in this case,MoviesAPI
andPersonalizationAPI
).Apollo Server calls this function for every incoming operation. It automatically assigns the returned object to the
dataSources
field of thecontext
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:
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 InMemoryLRUCache
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.
Using Memcached/Redis as a cache storage 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 or Redis as cache stores via the apollo-server-cache-memcached
and apollo-server-cache-redis
packages. You can specify which one to use by creating an instance and passing it into the ApolloServer
constructor.
Memcached
1const { MemcachedCache } = require('apollo-server-cache-memcached');
2
3const server = new ApolloServer({
4 typeDefs,
5 resolvers,
6 cache: new MemcachedCache(
7 ['memcached-server-1', 'memcached-server-2', 'memcached-server-3'],
8 { retries: 10, retry: 10000 }, // Options
9 ),
10 dataSources: () => ({
11 moviesAPI: new MoviesAPI(),
12 }),
13});
For the options you can pass to the underlying Memcached client, see the documentation.
Redis
1const { BaseRedisCache } = require('apollo-server-cache-redis');
2const Redis = require('ioredis');
3
4const server = new ApolloServer({
5 typeDefs,
6 resolvers,
7 cache: new BaseRedisCache({
8 client: new Redis({
9 host: 'redis-server',
10 }),
11 }),
12 dataSources: () => ({
13 moviesAPI: new MoviesAPI(),
14 }),
15});
For the options you can pass to the underlying Redis client, see the documentation.
Implementing your own cache backend
You can create your own implementation of the KeyValueCache
interface to connect to other caching data stores, or to optimize for your application's query characteristics.
For more information, see the README in for apollo-server-caching.
RESTDataSource
reference
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:
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.
Example
Here's an example RESTDataSource
subclass that defines two data-fetching methods, getMovie
and getMostViewedMovies
:
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/${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
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/${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/${movie.id}`, // path
42 );
43 }
44}
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 useful for storing a user token or other relevant information.
Setting a header
1class PersonalizationAPI extends RESTDataSource {
2 willSendRequest(request) {
3 request.headers.set('Authorization', this.context.token);
4 }
5}
Adding a query parameter
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:
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 your REST API's base URL based on the environment or other contextual values. You can use a getter for this:
1get baseURL() {
2 if (this.context.env === 'development') {
3 return 'https://movies-api-dev.example.com/';
4 } else {
5 return 'https://movies-api.example.com/';
6 }
7}
If you need more customization, including the ability to resolve a URL asynchronously, you can also override resolveURL
:
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
:
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 }