Fetching Data
Manage connections to databases and other data sources
Looking to fetch data from a REST API? Check out Fetching from REST.
Apollo Server can fetch data from any source you need, such as a REST API or a database. Your server can use any number of different data sources:
Because your server can use multiple various data sources, keeping your resolvers tidy becomes even more important.
For this reason, we recommend creating individual data source classes to encapsulate the logic of fetching data from a particular source, providing methods that resolvers can use to access data neatly. You can additionally customize your data source classes to help with caching, deduplication, or errors while resolving operations.
Creating data source classes
Your data source class can be as straightforward or complex as you need it to be. You know what data your server needs, and you can let that be the guide for the methods your class includes.
Below is an example of a data source class that connects to a database storing reservations:
1export class ReservationsDataSource {
2 private dbConnection;
3 private token;
4 private user;
5
6 constructor(options: { token: string }) {
7 this.dbConnection = this.initializeDBConnection();
8 this.token = options.token;
9 }
10
11 async initializeDBConnection() {
12 // set up our database details, instantiate our connection,
13 // and return that database connection
14 return dbConnection;
15 }
16
17 async getUser() {
18 if (!this.user) {
19 // store the user, lookup by token
20 this.user = await this.dbConnection.User.findByToken(this.token);
21 }
22 return this.user;
23 }
24
25 async getReservation(reservationId) {
26 const user = await this.getUser();
27 if (user) {
28 return await this.dbConnection.Reservation.findByPk(reservationId);
29 } else {
30 // handle invalid user
31 }
32 }
33
34 //... more methods for finding and creating reservations
35}
Apollo's
RestDataSource
class is a built-out example of how a data source class can handle caching, deduplication, and errors.
Batching and caching
If you want to add batching, deduplication, or caching to your data source class, we recommend using the DataLoader package. Using a package like DataLoader is particularly helpful for solving the infamous N+1 query problem.
DataLoader provides a memoization cache, which avoids loading the same object multiple times during a single GraphQL request (much like one of RESTDataSource
's caching layers). It also combines loads during a single event loop tick into a batched request that fetches multiple objects at once.
DataLoader instances are per-request, so if you use a DataLoader in your data source, ensure you create a new instance of that class with every request :
1import DataLoader from 'dataloader';
2
3class ProductsDataSource {
4 private dbConnection;
5
6 constructor(dbConnection) {
7 this.dbConnection = dbConnection;
8 }
9
10 private batchProducts = new DataLoader(async (ids) => {
11 const productList = await this.dbConnection.fetchAllKeys(ids);
12 // Dataloader expects you to return a list with the results ordered just like the list in the arguments were
13 // Since the database might return the results in a different order the following code sorts the results accordingly
14 const productIdToProductMap = productList.reduce((mapping, product) => {
15 mapping[product.id] = product;
16 return mapping;
17 }, {});
18 return ids.map((id) => productIdToProductMap[id]);
19 });
20
21 async getProductFor(id) {
22 return this.batchProducts.load(id);
23 }
24}
25
26// In your server file
27
28// Set up our database, instantiate our connection,
29// and return that database connection
30const dbConnection = initializeDBConnection();
31
32const { url } = await startStandaloneServer(server, {
33 context: async () => {
34 return {
35 dataSources: {
36 // Create a new instance of our data source for every request!
37 // (We pass in the database connection because we don't need
38 // a new connection for every request.)
39 productsDb: new ProductsDataSource(dbConnection),
40 },
41 };
42 },
43});
Adding data sources to your context
function
In the examples below, we use top-level
await
calls to start our server asynchronously. Check out our Getting Started guide to see how we configured our project to support this.
You can add data sources to your server's context
initialization function, like so:
1interface ContextValue {
2 dataSources: {
3 dogsDB: DogsDataSource;
4 catsApi: CatsAPI;
5 };
6 token: string;
7}
8
9const server = new ApolloServer<ContextValue>({
10 typeDefs,
11 resolvers,
12});
13
14const { url } = await startStandaloneServer(server, {
15 context: async ({ req }) => {
16 const { cache } = server;
17 const token = req.headers.token;
18 return {
19 // We create new instances of our data sources with each request.
20 // We can pass in our server's cache, contextValue, or any other
21 // info our data sources require.
22 dataSources: {
23 dogsDB: new DogsDataSource({ cache, token }),
24 catsApi: new CatsAPI({ cache }),
25 },
26 token,
27 };
28 },
29});
30
31console.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 data source classes (in this case,DogsDataSource
andCatsAPI
).If your data source is stateful (e.g., uses an in-memory cache), the
context
function should create a new instance of your data source class for each operation. This ensures that your data source doesn't accidentally cache results across requests.
Your resolvers can then access your data sources from the shared contextValue
object and use them to fetch data:
1const resolvers = {
2 Query: {
3 dog: async (_, { id }, { dataSources }) => {
4 return dataSources.dogsDB.getDog(id);
5 },
6 popularDogs: async (_, __, { dataSources }) => {
7 return dataSources.dogsDB.getMostLikedDogs();
8 },
9 bigCats: async (_, __, { dataSources }) => {
10 return dataSources.catsApi.getCats({ size: 10 });
11 },
12 },
13};
Open-source implementations
Apollo Server 3 contained an abstract class named DataSource
that each of your data sources could subclass. You'd then initialize each of your DataSource
subclasses using a special dataSources
function, which attaches your data sources to your context
behind the scenes.
In Apollo Server 4, you can now create your data sources in the same context
function as the rest of your per-request setup, avoiding the DataSource
superclass entirely. We recommend making a custom class for each data source, with each class best suited for that particular source of data.
Modern data sources
Apollo maintains the following open-source data source for Apollo Server 4:
Class | Examples | For Use With |
---|---|---|
RESTDataSource | See Fetching Rest | HTTP/REST APIs |
The community maintains the following open-source data sources for Apollo Server 4:
Class | Source | For Use With |
---|---|---|
BatchedSQLDataSource | Community | SQL databases (via Knex.js) & Batching (via DataLoader) |
FirestoreDataSource | Community | Cloud Firestore |
Legacy data source classes
⚠️ Note: The community built each data source package below for use with Apollo Server 3. As shown below, you can still use these packages in Apollo Server 4 with a bit of extra setup.
The below data source implementations extend the generic DataSource
abstract class, from the deprecated apollo-datasource
package. Subclasses of DataSource
define the logic required to communicate with a particular store or API.
The larger community maintains the following open-source implementations:
Class | Source | For Use With |
---|---|---|
HTTPDataSource | Community | HTTP/REST APIs |
SQLDataSource | Community | SQL databases (via Knex.js) |
MongoDataSource | Community | MongoDB |
CosmosDataSource | Community | Azure Cosmos DB |
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.
Using DataSource
subclasses
In Apollo Server 3, immediately after constructing each DataSource
subclass, your server would invoke the initialize({ cache, context })
method on each new DataSource
behind the scenes.
To replicate this in Apollo Sever 4, you can manually invoke the initialize
method in the constructor function of each DataSource
subclass, like so:
1import { ApolloServer } from '@apollo/server';
2import { startStandaloneServer } from '@apollo/server/standalone';
3import { KeyValueCache } from '@apollo/utils.keyvaluecache';
4import { Pool } from 'undici';
5import { HTTPDataSource } from 'apollo-datasource-http';
6
7class MoviesAPI extends HTTPDataSource {
8 override baseURL = 'https://movies-api.example.com/';
9
10 constructor(options: { cache: KeyValueCache<string>; token: string }) {
11 // the necessary arguments for HTTPDataSource
12 const pool = new Pool(baseURL);
13 super(baseURL, { pool });
14
15 // We need to call the initialize method in our data source's
16 // constructor, passing in our cache and contextValue.
17 this.initialize({ cache: options.cache, context: options.token });
18 }
19
20 async getMovie(id: string): Promise<Movie> {
21 return this.get<Movie>(`movies/${encodeURIComponent(id)}`);
22 }
23}
24
25interface MyContext {
26 dataSources: {
27 moviesApi: MoviesAPI;
28 };
29 token?: string;
30}
31
32const server = new ApolloServer<MyContext>({
33 typeDefs,
34 resolvers,
35});
36
37const { url } = await startStandaloneServer(server, {
38 context: async ({ req }) => {
39 const { cache } = server;
40 const token = req.headers.token;
41 return {
42 dataSources: {
43 moviesApi: new MoviesAPI({ cache, token }),
44 },
45 token,
46 };
47 },
48});