Docs
Launch GraphOS Studio

Fetching data

Manage connections to databases and other data sources


Looking to fetch data from a REST API? Check out Fetching from REST.

can fetch data from any source you need, such as a REST API or a database. Your server can use any number of different :

ApolloServer
Fetches data
Fetches data
Fetches data
Sends query
RESTDataSource
MongoDBSource
SQLDBSource
REST API
MongoDB Database
SQL Database
ApolloClient

Because your server can use multiple various data sources, keeping your resolvers tidy becomes even more important.

For this reason, we recommend creating individual classes to encapsulate the logic of fetching data from a particular source, providing methods that can use to access data neatly. You can additionally customize your data source classes to help with caching, deduplication, or errors while resolving .

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:

reservations.ts
export class ReservationsDataSource {
private dbConnection;
private token;
private user;
constructor(options: { token: string }) {
this.dbConnection = this.initializeDBConnection();
this.token = options.token;
}
async initializeDBConnection() {
// set up our database details, instantiate our connection,
// and return that database connection
return dbConnection;
}
async getUser() {
if (!this.user) {
// store the user, lookup by token
this.user = await this.dbConnection.User.findByToken(this.token);
}
return this.user;
}
async getReservation(reservationId) {
const user = await this.getUser();
if (user) {
return await this.dbConnection.Reservation.findByPk(reservationId);
} else {
// handle invalid user
}
}
//... more methods for finding and creating reservations
}
reservations.js
export class ReservationsDataSource {
constructor(options) {
this.dbConnection = this.initializeDBConnection();
this.token = options.token;
}
async initializeDBConnection() {
// set up our database details, instantiate our connection,
// and return that database connection
return dbConnection;
}
async getUser() {
if (!this.user) {
// store the user, lookup by token
this.user = await this.dbConnection.User.findByToken(this.token);
}
return this.user;
}
async getReservation(reservationId) {
const user = await this.getUser();
if (user) {
return await this.dbConnection.Reservation.findByPk(reservationId);
} else {
// handle invalid user
}
}
//... more methods for finding and creating reservations
}

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

import DataLoader from 'dataloader';
class ProductsDataSource {
private dbConnection;
constructor(dbConnection) {
this.dbConnection = dbConnection;
}
private batchProducts = new DataLoader(async (ids) => {
const productList = await this.dbConnection.fetchAllKeys(ids);
// Dataloader expects you to return a list with the results ordered just like the list in the arguments were
// Since the database might return the results in a different order the following code sorts the results accordingly
const productIdToProductMap = productList.reduce((mapping, product) => {
mapping[product.id] = product;
return mapping;
}, {});
return ids.map((id) => productIdToProductMap[id]);
});
async getProductFor(id) {
return this.batchProducts.load(id);
}
}
// In your server file
// Set up our database, instantiate our connection,
// and return that database connection
const dbConnection = initializeDBConnection();
const { url } = await startStandaloneServer(server, {
context: async () => {
return {
dataSources: {
// Create a new instance of our data source for every request!
// (We pass in the database connection because we don't need
// a new connection for every request.)
productsDb: new ProductsDataSource(dbConnection),
},
};
},
});
import DataLoader from 'dataloader';
class ProductsDataSource {
constructor(dbConnection) {
this.dbConnection = dbConnection;
}
batchProducts = new DataLoader(async (ids) => {
const productList = await this.dbConnection.fetchAllKeys(ids);
// Dataloader expects you to return a list with the results ordered just like the list in the arguments were
// Since the database might return the results in a different order the following code sorts the results accordingly
const productIdToProductMap = productList.reduce((mapping, product) => {
mapping[product.id] = product;
return mapping;
}, {});
return ids.map((id) => productIdToProductMap[id]);
});
async getProductFor(id) {
return this.batchProducts.load(id);
}
}
// In your server file
// Set up our database, instantiate our connection,
// and return that database connection
const dbConnection = initializeDBConnection();
const { url } = await startStandaloneServer(server, {
context: async () => {
return {
dataSources: {
// Create a new instance of our data source for every request!
// (We pass in the database connection because we don't need
// a new connection for every request.)
productsDb: new ProductsDataSource(dbConnection),
},
};
},
});

Adding data sources to your context function

NOTE

In the examples below, we use top-level await calls to start our server asynchronously. If you'd like to see how we set this up, check out the Getting Started guide for details.

You can add data sources to your server's context initialization function, like so:

index.ts
interface ContextValue {
dataSources: {
dogsDB: DogsDataSource;
catsApi: CatsAPI;
};
token: string;
}
const server = new ApolloServer<ContextValue>({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const { cache } = server;
const token = req.headers.token;
return {
// We create new instances of our data sources with each request.
// We can pass in our server's cache, contextValue, or any other
// info our data sources require.
dataSources: {
dogsDB: new DogsDataSource({ cache, token }),
catsApi: new CatsAPI({ cache }),
},
token,
};
},
});
console.log(`🚀 Server ready at ${url}`);
index.js
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const { cache } = server;
const token = req.headers.token;
return {
// We create new instances of our data sources with each request.
// We can pass in our server's cache, contextValue, or any other
// info our data sources require.
dataSources: {
dogsDB: new DogsDataSource({ cache, token }),
catsApi: new CatsAPI({ cache }),
},
token,
};
},
});
console.log(`🚀 Server ready at ${url}`);

Apollo Server calls the context initialization function for every incoming operation. This means:

  • For every , context returns an object containing new instances of your data source classes (in this case, DogsDataSource and CatsAPI).
  • 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:

resolvers.ts
const resolvers = {
Query: {
dog: async (_, { id }, { dataSources }) => {
return dataSources.dogsDB.getDog(id);
},
popularDogs: async (_, __, { dataSources }) => {
return dataSources.dogsDB.getMostLikedDogs();
},
bigCats: async (_, __, { dataSources }) => {
return dataSources.catsApi.getCats({ size: 10 });
},
},
};

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:

ClassExamplesFor Use With
RESTDataSourceSee Fetching RestHTTP/REST APIs

The community maintains the following open-source data sources for Apollo Server 4:

ClassSourceFor Use With
BatchedSQLDataSourceCommunitySQL databases (via Knex.js) & Batching (via DataLoader)
FirestoreDataSourceCommunityCloud 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:

ClassSourceFor Use With
HTTPDataSourceCommunityHTTP/REST APIs
SQLDataSourceCommunitySQL databases (via Knex.js)
MongoDataSourceCommunityMongoDB
CosmosDataSourceCommunityAzure 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:

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { KeyValueCache } from '@apollo/utils.keyvaluecache';
import { Pool } from 'undici';
import { HTTPDataSource } from 'apollo-datasource-http';
class MoviesAPI extends HTTPDataSource {
override baseURL = 'https://movies-api.example.com/';
constructor(options: { cache: KeyValueCache<string>; token: string }) {
// the necessary arguments for HTTPDataSource
const pool = new Pool(baseURL);
super(baseURL, { pool });
// We need to call the initialize method in our data source's
// constructor, passing in our cache and contextValue.
this.initialize({ cache: options.cache, context: options.token });
}
async getMovie(id: string): Promise<Movie> {
return this.get<Movie>(`movies/${encodeURIComponent(id)}`);
}
}
interface MyContext {
dataSources: {
moviesApi: MoviesAPI;
};
token?: string;
}
const server = new ApolloServer<MyContext>({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const { cache } = server;
const token = req.headers.token;
return {
dataSources: {
moviesApi: new MoviesAPI({ cache, token }),
},
token,
};
},
});
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { Pool } from 'undici';
import { HTTPDataSource } from 'apollo-datasource-http';
class MoviesAPI extends HTTPDataSource {
baseURL = 'https://movies-api.example.com/';
constructor(options) {
// the necessary arguments for HTTPDataSource
const pool = new Pool(baseURL);
super(baseURL, { pool });
// We need to call the initialize method in our data source's
// constructor, passing in our cache and contextValue.
this.initialize({ cache: options.cache, context: options.token });
}
async getMovie(id) {
return this.get(`movies/${encodeURIComponent(id)}`);
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const { cache } = server;
const token = req.headers.token;
return {
dataSources: {
moviesApi: new MoviesAPI({ cache, token }),
},
token,
};
},
});
Previous
Subscriptions
Next
REST APIs
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company