| title | Fetching Data |
|---|---|
| description | Manage connections to databases and other data sources |
import TopLevelAwait from '../shared/top-level-await.mdx';
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:
flowchart LR;
restAPI(REST API);
mongo(MongoDB Database);
sql(SQL Database);
subgraph ApolloServer;
restDataSource(RESTDataSource);
mongoDBDataSource(MongoDBSource);
sqlDataSource(SQLDBSource);
end
restDataSource --Fetches data--> restAPI;
mongoDBDataSource --Fetches data--> mongo;
sqlDataSource --Fetches data--> sql;
client(ApolloClient);
client --Sends query--> ApolloServer;
Learn how Apollo Connectors simplify incorporating REST APIs into your graph.
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.
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:
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
}Apollo's
RestDataSourceclass is a built-out example of how a data source class can handle caching, deduplication, and errors.
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 :
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), //highlight-line
},
};
},
});You can add data sources to your server's context initialization function, like so:
//highlight-start
interface ContextValue {
dataSources: {
dogsDB: DogsDataSource;
catsApi: CatsAPI;
};
token: string;
}
//highlight-end
const server = new ApolloServer<ContextValue>({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const { cache } = server; // highlight-line
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.
// highlight-start
dataSources: {
dogsDB: new DogsDataSource({ cache, token }),
catsApi: new CatsAPI({ cache }),
},
//highlight-end
token,
};
},
});
console.log(`🚀 Server ready at ${url}`);Apollo Server calls the context initialization function for every incoming operation. This means:
- For every operation,
contextreturns an object containing new instances of your data source classes (in this case,DogsDataSourceandCatsAPI). - If your data source is stateful (e.g., uses an in-memory cache), the
contextfunction 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:
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 });
},
},
};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.
Since 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.
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 |
⚠️ 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.
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 Server 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/';
//highlight-start
constructor(options: { cache: KeyValueCache<string>; token: string }) {
//highlight-end
// 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.
//highlight-start
this.initialize({ cache: options.cache, context: options.token });
//highlight-end
}
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 }) => {
//highlight-start
const { cache } = server;
const token = req.headers.token;
return {
dataSources: {
moviesApi: new MoviesAPI({ cache, token }),
},
//highlight-end
token,
};
},
});