Docs
Launch GraphOS Studio
Apollo Server 2 is officially end-of-life as of 22 October 2023. Learn more about upgrading.

Data sources

Manage connections to databases and REST APIs


Data sources are classes that 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 .

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

ApolloServer
Fetches data
Fetches data
Sends query
RESTDataSource
SQLDataSource
REST API
SQL Database
ApolloClient

Open-source implementations

All 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

to be added to the list!

ClassSourceFor Use With
RESTDataSource
ApolloREST APIs (
see below
)
HTTPDataSource
CommunityHTTP/REST APIs (newer community alternative to RESTDataSource)
SQLDataSource
CommunitySQL databases (via
Knex.js
)
MongoDataSource
CommunityMongoDB
CosmosDataSource
CommunityAzure Cosmos DB
FirestoreDataSource
CommunityCloud 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:

index.js
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => {
return {
moviesAPI: new MoviesAPI(),
personalizationAPI: new PersonalizationAPI(),
};
},
});
  • 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).
  • calls this function for every incoming operation. It automatically assigns the returned object to the dataSources of
    the context object
    that's passed between your server's .
  • Also as shown, the function should create a new instance of each data source for each operation. If multiple share a single instance, you might accidentally combine results from multiple operations.

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

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

By default, implementations use 's

to store the results of past fetches.

When you initialize , you can provide its constructor a different cache object that implements the

. 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.

supports using

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

const { MemcachedCache } = require('apollo-server-cache-memcached');
const server = new ApolloServer({
typeDefs,
resolvers,
cache: new MemcachedCache(
['memcached-server-1', 'memcached-server-2', 'memcached-server-3'],
{ retries: 10, retry: 10000 }, // Options
),
dataSources: () => ({
moviesAPI: new MoviesAPI(),
}),
});

For the options you can pass to the underlying Memcached client,

.

Redis

Redis
const { BaseRedisCache } = require('apollo-server-cache-redis');
const Redis = require('ioredis');
const server = new ApolloServer({
typeDefs,
resolvers,
cache: new BaseRedisCache({
client: new Redis({
host: 'redis-server',
}),
}),
dataSources: () => ({
moviesAPI: new MoviesAPI(),
}),
});

For the options you can pass to the underlying Redis client,

.

Implementing your own cache backend

You can create your own implementation of the

to connect to other caching data stores, or to optimize for your application's characteristics.

For more information, see the README in for

.

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:

npm install apollo-datasource-rest

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

Example

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

movies-api.js
const { RESTDataSource } = require('apollo-datasource-rest');
class MoviesAPI extends RESTDataSource {
constructor() {
// Always call super()
super();
// Sets the base URL for the REST API
this.baseURL = 'https://movies-api.example.com/';
}
async getMovie(id) {
// Send a GET request to the specified endpoint
return this.get(`movies/${id}`);
}
async getMostViewedMovies(limit = 10) {
const data = await this.get('movies', {
// Query parameters
per_page: limit,
order_by: 'most_viewed',
});
return data.results;
}
}

HTTP Methods

RESTDataSource includes convenience methods for common REST API request methods: get, post, put, patch, and delete (

).

An example of each is shown below:

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 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,

.

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 parameters. This method is most commonly used for authorization or other concerns that apply to all sent requests.

also have access to the context, which useful for storing a user token or other relevant information.

Setting a header

class PersonalizationAPI extends RESTDataSource {
willSendRequest(request) {
request.headers.set('Authorization', this.context.token);
}
}

Adding a query parameter

class PersonalizationAPI extends RESTDataSource {
willSendRequest(request) {
request.params.set('api_key', this.context.token);
}
}

Using with TypeScript

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

import { RESTDataSource, RequestOptions } from 'apollo-datasource-rest';
class PersonalizationAPI extends RESTDataSource {
baseURL = 'https://personalization-api.example.com/';
willSendRequest(request: RequestOptions) {
request.headers.set('Authorization', this.context.token);
}
}

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:

get baseURL() {
if (this.context.env === 'development') {
return 'https://movies-api-dev.example.com/';
} else {
return 'https://movies-api.example.com/';
}
}

If you need more customization, including the ability to resolve a URL asynchronously, you can also override resolveURL:

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

Using with DataLoader

The

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 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 over REST APIs, it's most helpful to have a resource cache that:

  • Saves data across multiple requests
  • Can be shared across multiple s
  • 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:

class PersonalizationAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://personalization-api.example.com/';
}
willSendRequest(request) {
request.headers.set('Authorization', this.context.token);
}
private progressLoader = new DataLoader(async (ids) => {
const progressList = await this.get('progress', {
ids: ids.join(','),
});
return ids.map(id =>
progressList.find((progress) => progress.id === id),
);
});
async getProgressFor(id) {
return this.progressLoader.load(id);
}
Previous
Resolvers
Next
Error handling
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company