diff --git a/docs/source/config.json b/docs/source/config.json index 62436491a3d..c93e64f8ee4 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -17,6 +17,7 @@ }, "Fetching Data": { "Resolvers": "/data/resolvers", + "Fetching from REST": "/data/fetching-rest", "Error handling": "/data/errors", "Subscriptions": "/data/subscriptions" }, diff --git a/docs/source/data/data-sources.mdx b/docs/source/data/data-sources.mdx deleted file mode 100644 index 00667ac7d80..00000000000 --- a/docs/source/data/data-sources.mdx +++ /dev/null @@ -1,321 +0,0 @@ ---- -title: Data sources -description: 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. - -```mermaid -flowchart LR; - restAPI(REST API); - sql(SQL Database); - subgraph ApolloServer; - restDataSource(RESTDataSource); - sqlDataSource(SQLDataSource); - end - restDataSource --Fetches data--> restAPI; - sqlDataSource --Fetches data--> sql; - client(ApolloClient); - client --Sends query--> ApolloServer; - class restAPI,sql secondary; -``` - -## Open-source implementations - -All data source implementations extend the generic [`DataSource` abstract class](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-datasource/src/index.ts), which is included in the `apollo-datasource` package. Subclasses of a `DataSource` should define whatever logic is required to communicate with a particular store or API. - -Apollo and the larger community maintain the following open-source implementations: - -> Do you maintain a `DataSource` implementation that isn't listed here? Please [submit a PR](https://github.com/apollographql/apollo-server/blob/main/docs/source/data/data-sources.md) to be added to the list! - -| Class | Source | For Use With | -|------------------|-----------|-----------------------| -| [`RESTDataSource`](https://github.com/apollographql/apollo-server/tree/main/packages/apollo-datasource-rest) | Apollo | REST APIs ([see below](#restdatasource-reference)) | -| [`HTTPDataSource`](https://github.com/StarpTech/apollo-datasource-http) | Community | HTTP/REST APIs (newer community alternative to `RESTDataSource`) | -| [`SQLDataSource`](https://github.com/cvburgess/SQLDataSource) | Community | SQL databases (via [Knex.js](http://knexjs.org/)) | -| [`MongoDataSource`](https://github.com/GraphQLGuide/apollo-datasource-mongodb/) | Community | MongoDB | -| [`CosmosDataSource`](https://github.com/andrejpk/apollo-datasource-cosmosdb) | Community | Azure Cosmos DB | -| [`FirestoreDataSource`](https://github.com/swantzter/apollo-datasource-firestore) | 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: - -```ts {4-9} title="index.js" -const server = new ApolloServer({ - typeDefs, - resolvers, - csrfPrevention: true, - cache: 'bounded', - plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })], - 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`). -- Apollo Server calls this function for _every incoming operation_. It automatically assigns the returned object to the `dataSources` field of [the `context` object](./resolvers/#the-context-argument) 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: - -```ts title="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, data source implementations use Apollo Server's in-memory cache 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](https://github.com/apollographql/apollo-utils/tree/main/packages/keyValueCache#keyvaluecache-interface). This enables you to back your cache with shared stores like Memcached or Redis. - -### Using an external cache 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](https://memcached.org/), [Redis](https://redis.io/), or other cache backends via the [`keyv`](https://www.npmjs.com/package/keyv) package. For examples, see [Configuring external caching](../performance/cache-backends#configuring-external-caching). - -You can also choose to implement your own cache backend. For more information, see [Implementing your own cache backend](../performance/cache-backends#implementing-your-own-cache-backend). - -## `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: - -```bash -npm 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`: - -```ts title="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/${encodeURIComponent(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` ([see the source](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-datasource-rest/src/RESTDataSource.ts#L154-L202)). - -An example of each is shown below: - - - -```ts -class MoviesAPI extends RESTDataSource { - constructor() { - super(); - this.baseURL = 'https://movies-api.example.com/'; - } - - // GET - async getMovie(id) { - return this.get( - `movies/${encodeURIComponent(id)}`, // path - ); - } - - // POST - async postMovie(movie) { - return this.post( - `movies`, // path - movie, // request body - ); - } - - // PUT - async newMovie(movie) { - return this.put( - `movies`, // path - movie, // request body - ); - } - - // PATCH - async updateMovie(movie) { - return this.patch( - `movies`, // path - { id: movie.id, movie }, // request body - ); - } - - // DELETE - async deleteMovie(movie) { - return this.delete( - `movies/${encodeURIComponent(movie.id)}`, // path - ); - } -} -``` - - - -> Note the use of [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). This is a standard JavaScript function that encodes special characters in a URI, preventing a possible injection attack vector. - -For a simple example, suppose our REST endpoint responded to the following URLs: - -- DELETE `/movies/:id` -- DELETE `/movies/:id/characters` - -A "malicious" client could provide an `:id` of `1/characters` to target the delete `characters` endpoint when it was the singular `movie` endpoint that we were trying to delete. URI encoding prevents this kind of injection by transforming the `/` into `%2F`. This can then be correctly decoded and interpreted by the server and won't be treated as a path segment. - -#### 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](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters). - -### 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 is useful for storing a user token or other relevant information. - -#### Setting a header - -```ts -class PersonalizationAPI extends RESTDataSource { - willSendRequest(request) { - request.headers.set('Authorization', this.context.token); - } -} -``` - -#### Adding a query parameter - -```ts -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: - -```typescript -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 the URL based on the environment or other contextual values. To do this, you can override `resolveURL`: - -```ts -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 [DataLoader](https://github.com/graphql/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`: - -```ts -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); - } -``` diff --git a/docs/source/data/fetching-data.mdx b/docs/source/data/fetching-data.mdx new file mode 100644 index 00000000000..b1ca89529b0 --- /dev/null +++ b/docs/source/data/fetching-data.mdx @@ -0,0 +1,6 @@ +--- +title: Fetching Data +description: Fetching data from data sources besides REST +--- + + diff --git a/docs/source/data/fetching-rest.mdx b/docs/source/data/fetching-rest.mdx new file mode 100644 index 00000000000..f6d66f3d806 --- /dev/null +++ b/docs/source/data/fetching-rest.mdx @@ -0,0 +1,432 @@ +--- +title: Fetching from REST +description: Using RESTDataSource to fetch data from REST APIs +--- + +> See the [`@apollo/datasource-rest` page](https://github.com/apollographql/datasource-rest) for the full details of the `RESTDataSource` API. + +The `RESTDataSource` class helps you fetch data from REST APIs. The `RESTDataSource` class helps handle caching, deduplication, and errors while resolving operations. + +```mermaid +flowchart LR; + restAPI(REST API); + subgraph ApolloServer; + restDataSource(RESTDataSource); + end + restDataSource --Fetches data--> restAPI; + client(ApolloClient); + client --Sends query--> ApolloServer; +``` + + + +## Creating subclasses + +To get started, install the [`@apollo/datasource-rest` package](https://www.npmjs.com/package/@apollo/datasource-rest): + +```bash +npm install @apollo/datasource-rest +``` + +Your server should define a separate subclass of `RESTDataSource` for each REST API it communicates with. Here's an example of a `RESTDataSource` subclass that defines two data-fetching methods, `getMovie` and `getMostViewedMovies`: + + +```ts title="movies-api.ts" +import { RESTDataSource } from '@apollo/datasource-rest'; + +class MoviesAPI extends RESTDataSource { + override baseURL = 'https://movies-api.example.com/'; + + async getMovie(id): Promise { + return this.get(`movies/${encodeURIComponent(id)}`); + } + + async getMostViewedMovies(limit = '10'): Promise { + const data = await this.get('movies', { + params: { + per_page: limit, + order_by: 'most_viewed', + }, + }); + return data.results; + } +} +``` + + + +You can extend the `RESTDataSource` class to implement whatever data-fetching methods your resolvers need. These methods should use the built-in convenience methods (e.g., `get` and `post`) to perform HTTP requests, helping you add query parameters, parse and cache JSON results, dedupe requests, and handle errors. + +## Adding data sources to Apollo Server's context + +You can add data sources to the `context` initialization function, like so: + + + +```ts title="index.ts" +//highlight-start +interface ContextValue { + dataSources: { + moviesAPI: MoviesAPI; + personalizationAPI: PersonalizationAPI; + }; +} +//highlight-end + +const server = new ApolloServer({ + typeDefs, + resolvers, +}); + +const { url } = await startStandaloneServer(server, { + context: async () => { + const { cache } = server; // highlight-line + return { + // We create new instances of our data sources with each request, + // passing in our server's cache. + //highlight-start + dataSources: { + moviesAPI: new MoviesAPI({ cache }), + personalizationAPI: new PersonalizationAPI({ cache }), + }, + //highlight-end + }; + }, +}); + +console.log(`๐Ÿš€ Server ready at ${url}`); +``` + + + + +Apollo Server calls [the `context` initialization](./resolvers/#the-context-argument) function for _every incoming operation_. This means: +- For every operation, `context` returns an _object_ containing new instances of your `RESTDataSource` subclasses (in this case, `MoviesAPI` and `PersonalizationAPI`). +- The **`context` function should create a new instance of each `RESTDataSource` subclass for each operation.** + +Your resolvers can then access your data sources from the shared `context` object and use them to fetch data: + +```ts title="resolvers.ts" +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 + +> ๐Ÿ“ฃ **New in Apollo Server 4**: Apollo Server no longer automatically provides its cache to data sources. [See here for more details](#datasources). + +The `RESTDataSource` class can cache results if the REST API it fetches from specifies caching headers in its HTTP responses (e.g., [`cache-control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)). + +As shown in the above code snippet, by default, each `RESTDataSource` subclass accepts a `cache` argument (e.g., Apollo Server's default cache) to store the results of past fetches: + +```ts disableCopy +class MoviesAPI extends RESTDataSource { + override baseURL = 'https://movies-api.example.com/'; + + // We omit the constructor function here because + // RESTDataSource accepts a cache argument by default +} + +// server set up, etc. + +const { url } = await startStandaloneServer(server, { + context: async ({ req }) => { + const { cache } = server; // highlight-line + return { + dataSources: { + // highlight-start + moviesAPI: new MoviesAPI({ cache }), + personalizationAPI: new PersonalizationAPI({ cache }), + // highlight-end + }, + }; + }, +}); +``` + +If your `RESTDataSource` subclass accepts multiple arguments, make sure you add a constructor function, like so: + + + +```ts +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { RESTDataSource } from '@apollo/datasource-rest'; +// KeyValueCache is the type of Apollo server's default cache +import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; + +class PersonalizationAPI extends RESTDataSource { + override baseURL = 'https://movies-api.example.com/'; + private token: string; + + //highlight-start + constructor(options: { token: string; cache: KeyValueCache }) { + super(options); // this sends our server's `cache` through + this.token = options.token; + } + //highlight-end + + // data fetching methods, etc. +} + +// set up server, context typing, etc. + +const { url } = await startStandaloneServer(server, { + context: async ({ req }) => { + const token = getTokenFromRequest(req); + const { cache } = server; + return { + //highlight-start + dataSources: { + personalizationApi: new PersonalizationAPI({ cache, token }), + }, + //highlight-end + }; + }, +}); +``` + + + + +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. + +> If you want to configure or replace Apollo Sever's default cache, see [Configuring external caching](../performance/cache-backends) for more details. + +## HTTP Methods + +`RESTDataSource` includes convenience methods for common REST API request methods: `get`, `post`, `put`, `patch`, and `delete` ([see the source](https://github.com/apollographql/datasource-rest/blob/25862e18d8b35e324c150654c5686ed317b3fca8/src/RESTDataSource.ts#L163)). + +An example of each is shown below: + + + +```ts +class MoviesAPI extends RESTDataSource { + override baseURL = 'https://movies-api.example.com/'; + + // GET + async getMovie(id) { + return this.get( + `movies/${encodeURIComponent(id)}`, // path + ); + } + + // POST + async postMovie(movie) { + return this.post( + `movies`, // path + { body: { movie } }, // request body + ); + } + + // PUT + async newMovie(movie) { + return this.put( + `movies`, // path + { body: { movie } }, // request body + ); + } + + // PATCH + async updateMovie(movie) { + return this.patch( + `movies`, // path + { body: { id: movie.id, movie } }, // request body + ); + } + + // DELETE + async deleteMovie(movie) { + return this.delete( + `movies/${encodeURIComponent(movie.id)}`, // path + ); + } +} + +``` + + + +Note the use of [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) in the above snippet. This is a standard function that encodes special characters in a URI, preventing a possible injection attack vector. + +For a simple example, suppose our REST endpoint responded to the following URLs: + +- DELETE `/movies/:id` +- DELETE `/movies/:id/characters` + +A "malicious" client could provide an `:id` of `1/characters` to target the delete `characters` endpoint when it was the singular `movie` endpoint that we were trying to delete. URI encoding prevents this kind of injection by transforming the `/` into `%2F`. This can then be correctly decoded and interpreted by the server and won't be treated as a path segment. + +### 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 is an object where you can set a request's `headers`, `params`, `cacheOptions`, and `body`: + +```ts +class MoviesAPI extends RESTDataSource { + override baseURL = 'https://movies-api.example.com/'; + + // an example making an HTTP POST request + async postMovie(movie) { + return this.post( + `movies`, // path + { body: movie }, // request body + ); + } +} +``` + +## Intercepting fetches + +> **New in Apollo Server 4**: Apollo Server 4 now uses the [`@apollo/utils.fetcher`](../migration#apolloutilsfetcher-replaces-apollo-server-env) interface under the hood for fetching. This interface lets you choose your own implementation of the Fetch API. To ensure compatibility with all Fetch implementations, the request provided to hooks like `willSendRequest` is a plain JS object rather than a `Request` object with methods. + +`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 is useful for storing a user token or other relevant information. + +> If you're using TypeScript, make sure to import the `WillSendRequestOptions` type. + +### Setting a header + + + +```ts +import { RESTDataSource, WillSendRequestOptions } from '@apollo/datasource-rest'; +import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; + +class PersonalizationAPI extends RESTDataSource { + override baseURL = 'https://movies-api.example.com/'; + private token: string; + + constructor(options: { token: string; cache: KeyValueCache }) { + super(options); + this.token = options.token; + } + + // highlight-start + override willSendRequest(request: WillSendRequestOptions) { + request.headers['authorization'] = this.token; + } + // highlight-end +} +``` + + + +### Adding a query parameter + + + +```ts +import { RESTDataSource, WillSendRequestOptions } from '@apollo/datasource-rest'; +import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; + +class PersonalizationAPI extends RESTDataSource { + override baseURL = 'https://movies-api.example.com/'; + private token: string; + + constructor(options: { token: string; cache: KeyValueCache }) { + super(options); + this.token = options.token; + } + + // highlight-start + override willSendRequest(request: WillSendRequestOptions) { + request.params.set('api_key', this.token); + } + // highlight-end +} +``` + + + +## Resolving URLs dynamically + +In some cases, you'll want to set the URL based on the environment or other contextual values. To do this, you can override `resolveURL`: + + + +```ts +import { RESTDataSource, RequestOptions } from '@apollo/datasource-rest'; +import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; + +class PersonalizationAPI extends RESTDataSource { + private token: string; + + constructor(options: { token: string; cache: KeyValueCache }) { + super(options); + this.token = options.token; + } + + override async resolveURL(path: string, request: RequestOptions) { + if (!this.baseURL) { + const addresses = await resolveSrv(path.split('/')[1] + '.service.consul'); + this.baseURL = addresses[0]; + } + return super.resolveURL(path, request); + } +} + +``` + + + + +## Using with DataLoader + +The [DataLoader](https://github.com/graphql/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`: + +```ts +import DataLoader from 'dataloader'; +import { RESTDataSource, WillSendRequestOptions } from '@apollo/datasource-rest'; +import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; + +class PersonalizationAPI extends RESTDataSource { + override baseURL = 'https://movies-api.example.com/'; + private token: string; + + constructor(options: { token: string; cache: KeyValueCache }) { + super(options); // this should send our server's `cache` through + this.token = options.token; + } + + override willSendRequest(request: WillSendRequestOptions) { + request.headers['authorization'] = this.token; + } + + private progressLoader = new DataLoader(async (ids) => { + const progressList = await this.get('progress', { params: { ids: ids.join(',') } }); + return ids.map((id) => progressList.find((progress) => progress.id === id)); + }); + + async getProgressFor(id) { + return this.progressLoader.load(id); + } +} +``` diff --git a/docs/source/data/resolvers.mdx b/docs/source/data/resolvers.mdx index a3cbfc5dbe0..dcadf7bac40 100644 --- a/docs/source/data/resolvers.mdx +++ b/docs/source/data/resolvers.mdx @@ -26,7 +26,7 @@ We want to define resolvers for the `numberSix` and `numberSeven` fields of the Those resolver definitions look like this: -```ts +```ts const resolvers = { Query: { numberSix() { @@ -64,7 +64,7 @@ We want to be able to query the `user` field to fetch a user by its `id`. To achieve this, our server needs access to user data. For this contrived example, assume our server defines the following hardcoded array: -```ts +```ts const users = [ { id: '1', @@ -77,15 +77,9 @@ const users = [ ]; ``` - - Now we can define a resolver for the `user` field, like so: -```ts +```ts const resolvers = { Query: { user(parent, args, context, info) { @@ -165,6 +159,9 @@ console.log(`๐Ÿš€ Server listening at: ${url}`); Note that you can define your resolvers across as many different files and objects as you want, as long as you merge all of them into a single resolver map that's passed to the `ApolloServer` constructor. + +> To learn how to fetch data from a REST API, see [Fetching from REST](./fetching-rest/). + ## Resolver chains Whenever a query asks for a field that returns an object type, the query _also_ asks for _at least one field_ of that object (if it didn't, there would be no reason to include the object in the query). A query always "bottoms out" on fields that return a [scalar](../schema/schema/#scalar-types), an [enum](../schema/schema/#enum-types), or a list of these. @@ -383,13 +380,8 @@ Resolver functions are passed four arguments: `parent`, `args`, `context`, and ` ### The `context` argument -> โš ๏ธ The Apollo Server 4 alpha doesn't currently support using [`RESTDataSource`](../migration#datasources), a class commonly used to fetch data from a database or a REST API. This feature is in active development, so check back frequently for updates. - -The `context` argument is useful for passing things that any resolver might need, like [authentication scope](https://blog.apollographql.com/authorization-in-graphql-452b1c402a9), database connections, and custom fetch functions. - - + +The `context` argument is useful for passing things that any resolver might need, like [authentication scope](https://blog.apollographql.com/authorization-in-graphql-452b1c402a9), [sources for fetching data](./fetching-rest/), database connections, and custom fetch functions. If you're using [dataloaders to batch requests](./fetching-rest/#using-with-dataloader) across resolvers, you can attach them to the `context` as well. **Resolvers should never destructively modify the `context` argument.** This ensures consistency across all resolvers and prevents unexpected errors. @@ -407,7 +399,7 @@ const resolvers = { // Example resolver adminExample: (parent, args, context, info) => { if (context.authScope !== ADMIN) { - throw new GraphQLError('not admin!', { + throw new GraphQLError('not admin!', { extensions: { code: 'UNAUTHENTICATED' } }); } @@ -447,7 +439,7 @@ The `context` function should be *asynchronous* and return an object, which is t Because the `context` initialization function is asynchronous, you can use it to establish database connections and wait for other operations to complete: -```ts +```ts context: async () => ({ db: await client.connect(), }) diff --git a/docs/source/migration.mdx b/docs/source/migration.mdx index 4df38895c52..57d8544ec47 100644 --- a/docs/source/migration.mdx +++ b/docs/source/migration.mdx @@ -4,7 +4,7 @@ title: Migrating to Apollo Server 4 (Alpha) > โš ๏ธ **Apollo Server 4 is currently in public alpha.** It is not yet feature-complete, and breaking changes might occur between this release and general availability. [Learn about release stages.](/resources/release-stages/#open-source-release-stages) > -> This alpha primarily aims to enable community members to develop integrations between Apollo Server and their favorite web frameworks. Many Apollo Server 3 users **can't upgrade** to this alpha yet. For example, this alpha [removes multiple web framework integrations](#removed-integrations) and doesn't support using [`RESTDataSource`](#datasources). +> This alpha primarily aims to enable community members to develop integrations between Apollo Server and their favorite web frameworks. Many Apollo Server 3 users **can't upgrade** to this alpha yet. For example, this alpha [removes multiple web framework integrations](#removed-integrations). > > We are working on updating our documentation to reflect the changes introduced in Apollo Server 4. This article explains which features _do_ require code changes and how to make them. @@ -359,18 +359,12 @@ For background, Apollo Server uses type system features introduced in v4.7. We w If supporting older versions of TypeScript is important to you and you'd like to help us get `typesVersions` working, we'd appreciate PRs! - - ## Removed constructor options The following `ApolloServer` constructor options have been removed in favor of other features or configuration methods. ### `dataSources` -> โš ๏ธ This feature is in active development and **does not** currently work as described. -> -> The Apollo Server 4 alpha doesn't support importing from the [`apollo-datasource-rest`](https://www.npmjs.com/package/apollo-datasource-rest) package. We intend to [rename](https://github.com/apollographql/apollo-server/issues/6048) the `apollo-datasource-rest` package to `@apollo/datasource-rest` and update it to be fully compatible with Apollo Server 4. - In Apollo Server 3, the top-level [`dataSources` constructor option](/apollo-server/data/data-sources#adding-data-sources-to-apollo-server) essentially adds a post-processing step to your app's context function, creating `DataSource` subclasses and adding them to a `dataSources` field on your [`context`](/apollo-server/data/resolvers/#the-context-argument) object. This means the TypeScript type the `context` function returns is _different_ from the `context` type your resolvers and plugins receive. Additionally, this design obfuscates that `DataSource` objects are created once per request (i.e., like the rest of the context object). Apollo Server 4 removes the `dataSources` constructor option. You can now treat `DataSources` like any other part of your `context` object. @@ -381,19 +375,19 @@ For example, below, we use the `RESTDataSource` class to create a `DataSource` w -```ts {4, 26-35} title="Apollo Server 3" -import { RESTDataSource } from 'apollo-datasource-rest'; +```ts title="Apollo Server 3" +import { RESTDataSource, RequestOptions } from 'apollo-datasource-rest'; import { ApolloServer } from 'apollo-server'; -class MoviesAPI extends RESTDataSource { - baseURL = 'https://movies-api.example.com/'; +class MoviesAPI extends RESTDataSource { //highlight-line + override baseURL = 'https://movies-api.example.com/'; - willSendRequest(request: RequestOptions) { + override willSendRequest(request: RequestOptions) { request.headers.set('Authorization', this.context.token); } - async getMovie(id: string): Movie { - return this.get(`movies/${encodeURIComponent(id)}`); + async getMovie(id: string): Promise { + return this.get(`movies/${encodeURIComponent(id)}`); } } @@ -407,16 +401,18 @@ interface ContextValue { const server = new ApolloServer({ typeDefs, resolvers, - context: ({ req: ExpressRequest }): Omit => { + context: ({ req: ExpressRequest }): Omit => { //highlight-line return { token: getTokenFromRequest(req), }; }, + //highlight-start dataSources: (): ContextValue['dataSources'] => { return { moviesAPI: new MoviesAPI(), }; }, + //highlight-end }); await server.listen(); @@ -424,99 +420,108 @@ await server.listen(); - - -Below is how you write the same code in Apollo Server 4. Note that the `@apollo/datasource-rest` package is now renamed `@apollo/datasource-rest`. - -> โš ๏ธ This feature is in active development, and the below code snippets **do not** currently work as described. [See above for more details](#datasources). +Below is how you write the same code in Apollo Server 4. -```ts {5, 25-27, 38, 41-43} title="Apollo Server 4" -import { RESTDataSource, RESTDataSourceOptions } from '@apollo/datasource-rest'; +```ts title="Apollo Server 4" +import { RESTDataSource, WillSendRequestOptions } from '@apollo/datasource-rest'; +// KeyValueCache is the type of Apollo server's default cache +import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; -class MoviesAPI extends RESTDataSource { - baseURL = 'https://movies-api.example.com/'; +class MoviesAPI extends RESTDataSource { // highlight-line + override baseURL = 'https://movies-api.example.com/'; private token: string; - constructor(options: { token: string } & RESTDataSourceOptions) { - super(options); // this should send `cache` through + constructor(options: { token: string; cache: KeyValueCache }) { + super(options); // this sends our server's `cache` through this.token = options.token; } - willSendRequest(request: RequestOptions) { - request.headers.set('Authorization', this.token); + override willSendRequest(request: WillSendRequestOptions) { + request.headers['authorization'] = this.token; } - async getMovie(id: string): Movie { - return this.get(`movies/${encodeURIComponent(id)}`); + async getMovie(id: string): Promise { + return this.get(`movies/${encodeURIComponent(id)}`); } } +// highlight-start interface ContextValue { token: string; dataSources: { moviesAPI: MoviesAPI; - } -}; + }; +} +// highlight-end const server = new ApolloServer({ typeDefs, resolvers, }); -await startStandaloneServer(server, { +const { url } = await startStandaloneServer(server, { context: async ({ req }) => { const token = getTokenFromRequest(req); const { cache } = server; return { token, + //highlight-start dataSources: { moviesAPI: new MoviesAPI({ cache, token }), }, + //highlight-end }; }, }); + +console.log(`๐Ÿš€ Server ready at ${url}`); ``` +> For Apollo Server 4, `apollo-datasource-rest` package has been updated and renamed [`@apollo/datasource-rest`](https://www.npmjs.com/package/@apollo/datasource-rest). + If you want to access your entire context's value within your `DataSource`, you can do so by making your context value a `class` (enabling it to refer to itself via `this` in its constructor): -```ts {24-36, 38, 43} -import { RESTDataSource, RESTDataSourceOptions } from '@apollo/datasource-rest'; +```ts +import { RESTDataSource, WillSendRequestOptions } from '@apollo/datasource-rest'; //highlight-line +import { KeyValueCache } from '@apollo/utils.keyvaluecache'; import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; import { IncomingMessage } from 'http'; class MoviesAPI extends RESTDataSource { - baseURL = 'https://movies-api.example.com/'; + override baseURL = 'https://movies-api.example.com/'; private contextValue: ContextValue; - constructor(options: { contextValue: ContextValue } & RESTDataSourceOptions) { - super(options); // this should send `cache` through + constructor(options: { contextValue: ContextValue; cache: KeyValueCache }) { + super(options); // this should send `cache` through this.contextValue = options.contextValue; } - willSendRequest(request: RequestOptions) { - request.headers.set('Authorization', this.contextValue.token); + override willSendRequest(request: WillSendRequestOptions) { + request.headers['authorization'] = this.contextValue.token; } - async getMovie(id: string): Movie { - return this.get(`movies/${encodeURIComponent(id)}`); + async getMovie(id):Promise { + return this.get(`movies/${encodeURIComponent(id)}`); } } +// highlight-start class ContextValue { public token: string; public dataSources: { moviesAPI: MoviesAPI; - } - constructor({ req, server }: { req: IncomingMessage, server: ApolloServer }) { + }; + + constructor({ req, server }: { req: IncomingMessage; server: ApolloServer }) { this.token = getTokenFromRequest(req); const { cache } = server; this.dataSources = { @@ -524,13 +529,15 @@ class ContextValue { }; } } +// highlight-end const server = new ApolloServer({ typeDefs, resolvers, }); + await startStandaloneServer(server, { - context: async ({ req }) => new ContextValue({ req, server }), + context: async ({ req }) => new ContextValue({ req, server }), //highlight-line }); ``` @@ -1611,9 +1618,10 @@ new ApolloServer({ ## Renamed packages The following packages have been renamed in Apollo Server 4: - * `apollo-server-plugin-response-cache` is now [`@apollo/server-plugin-response-cache`](https://www.npmjs.com/package/@apollo/server-plugin-response-cache). - * `apollo-server-plugin-operation-registry` is now [`@apollo/server-plugin-operation-registry`](https://www.npmjs.com/package/@apollo/server-plugin-operation-registry). - * `apollo-reporting-protobuf` (an internal implementation detail for the usage reporting plugin) is now [`@apollo/usage-reporting-protobuf`](https://www.npmjs.com/package/@apollo/usage-reporting-protobuf). + - `apollo-datasource-rest` is now [`@apollo/datasource-rest`](https://www.npmjs.com/package/@apollo/datasource-rest). + - `apollo-server-plugin-response-cache` is now [`@apollo/server-plugin-response-cache`](https://www.npmjs.com/package/@apollo/server-plugin-response-cache). + - `apollo-server-plugin-operation-registry` is now [`@apollo/server-plugin-operation-registry`](https://www.npmjs.com/package/@apollo/server-plugin-operation-registry). + - `apollo-reporting-protobuf` (an internal implementation detail for the usage reporting plugin) is now [`@apollo/usage-reporting-protobuf`](https://www.npmjs.com/package/@apollo/usage-reporting-protobuf). Note that once Apollo Server 4 is released, all actively maintained Apollo packages will start with `@apollo/`. This leaves the `apollo-` namespace open for community integration packages (e.g., `apollo-server-integration-fastify`). diff --git a/docs/source/performance/cache-backends.mdx b/docs/source/performance/cache-backends.mdx index e83534a7477..e2c1de8d3f7 100644 --- a/docs/source/performance/cache-backends.mdx +++ b/docs/source/performance/cache-backends.mdx @@ -3,7 +3,7 @@ title: Configuring cache backends description: How to configure Apollo Server's cache --- -Many Apollo Server features take advantage of a cache backend (these features include [automatic persisted queries](./apq), the [response cache plugin](./caching#caching-with-responsecacheplugin-advanced), and [`RESTDataSource`](../data/data-sources#restdatasource-reference)). Apollo Server uses an in-memory cache by default, but you can configure it to use a different backend, such as Redis or Memcached. +Many Apollo Server features take advantage of a cache backend (these features include [automatic persisted queries](./apq), the [response cache plugin](./caching#caching-with-responsecacheplugin-advanced), and [`RESTDataSource`](../data/fetching-rest)). Apollo Server uses an in-memory cache by default, but you can configure it to use a different backend, such as Redis or Memcached. You can specify a cache backend by passing a `cache` option to the `ApolloServer` constructor. Your specified cache backend must implement the [`KeyValueCache`](https://github.com/apollographql/apollo-utils/tree/main/packages/keyValueCache#keyvaluecache-interface) interface from the `@apollo/utils.keyvaluecache` package. @@ -47,7 +47,7 @@ const server = new ApolloServer({ ## Configuring external caching -Apollo no longer maintains any caching backends directly. Instead, we recommend using the [`keyv`](https://www.npmjs.com/package/keyv) package along with the [`KeyvAdapter`](https://github.com/apollographql/apollo-utils/tree/main/packages/keyvAdapter#keyvadapter-class) class provided by the `@apollo/utils.keyvadapter` package. +Apollo no longer maintains any caching backends directly. Instead, we recommend using the [`keyv`](https://www.npmjs.com/package/keyv) package along with the [`KeyvAdapter`](https://github.com/apollographql/apollo-utils/tree/main/packages/keyvAdapter#keyvadapter-class) class provided by the `@apollo/utils.keyvadapter` package. `KeyvAdapter` wraps a `Keyv` instance and implements the `KeyValueCache` interface which is required by Apollo Server. You can use the `KeyvAdapter` class to wrap a `Keyv` instance and provide it to the `cache` option of the `ApolloServer` constructor like so: @@ -207,4 +207,4 @@ Versions of Apollo Server prior to 3.9 use the `apollo-server-caching` package t The `InMemoryLRUCache` class has also moved to the `@apollo/utils.keyvaluecache` package. The `InMemoryLRUCache` class now uses version 7 of `lru-cache`, accepting different configuration options and no longer allowing a cache to be unbounded. -The `apollo-server-cache-redis` and `apollo-server-cache-memcached` packages are no longer receiving updates; we recommend using `keyv` instead, as shown above. \ No newline at end of file +The `apollo-server-cache-redis` and `apollo-server-cache-memcached` packages are no longer receiving updates; we recommend using `keyv` instead, as shown above. diff --git a/docs/source/security/authentication.mdx b/docs/source/security/authentication.mdx index 8ab1ff4d083..dabd07b7efd 100644 --- a/docs/source/security/authentication.mdx +++ b/docs/source/security/authentication.mdx @@ -149,7 +149,7 @@ One choice to make when building out our resolvers is what an unauthorized field Now let's expand that example a little further, and only allow users with an `admin` role to look at our user list. After all, we probably don't want just anyone to have access to all our users. -```ts +```ts users: (parent, args, context) => { if (!context.user || !context.user.roles.includes('admin')) return null; return context.models.User.getAll(); @@ -166,11 +166,11 @@ As our server gets more complex, there will probably be multiple places in the s -As always, we recommend moving the actual data fetching and transformation logic from your resolvers to data sources or model objects that each represent a concept from your application: `User`, `Post`, etc. This allows you to make your resolvers a thin routing layer, and put all of your business logic in one place. +As always, we recommend moving the actual data fetching and transformation logic from your resolvers to [data sources](../data/fetching-rest/) or model objects that each represent a concept from your application: `User`, `Post`, etc. This allows you to make your resolvers a thin routing layer, and put all of your business logic in one place. For example, a model file for `User` would include all the logic for operating on users, and might look something like this: -```ts +```ts export const User = { getAll: () => { /* fetching/transformation logic for all users */ @@ -186,7 +186,7 @@ export const User = { In the following example, our schema has multiple ways to request a single user: -```graphql +```graphql type Query { user(id: ID!): User article(id: ID!): Article @@ -208,7 +208,7 @@ Rather than having the same fetching logic for a single user in two separate pla You may have noticed that our models also exist on the context, alongside the user object we added earlier. We can add the models to the context in exactly the same way as we did the user. -```ts +```ts context: async ({ req }) => { // get the user token from the headers const token = req.headers.authentication || ''; @@ -237,7 +237,7 @@ context: async ({ req }) => { Starting to generate our models with a function requires a small refactor, that would leave our User model looking something like this: -```ts +```ts export const generateUserModel = ({ user }) => ({ getAll: () => { /* fetching/transform logic for all users */ @@ -253,7 +253,7 @@ export const generateUserModel = ({ user }) => ({ Now any model method in `User` has access to the same `user` information that resolvers already had, allowing us to refactor the `getAll` function to do the permissions check directly rather than having to put it in the resolver: -```ts +```ts getAll: () => { if (!user || !user.roles.includes('admin')) return null; return fetch('http://myurl.com/users'); @@ -315,4 +315,4 @@ export const generateUserModel = ({ req }) => ({ }); ``` -If your REST endpoint is already backed by some form of authorization, this cuts down a lot of the logic that needs to get built in the GraphQL layer. This can be a great option when building a GraphQL API over an existing REST API that has everything you need already built in. \ No newline at end of file +If your REST endpoint is already backed by some form of authorization, this cuts down a lot of the logic that needs to get built in the GraphQL layer. This can be a great option when building a GraphQL API over an existing REST API that has everything you need already built in.