Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@apollo/datasource-rest

apollographql429.7kMIT6.4.1TypeScript support: included

REST DataSource for Apollo Server v4

readme

Apollo REST Data Source

This package exports a (RESTDataSource) class which is used for fetching data from a REST API and exposing it via GraphQL within Apollo Server.

RESTDataSource wraps an implementation of the DOM-style Fetch API such as node-fetch and adds the following features:

  • Two layers of caching:
    • An in-memory "request deduplication" feature which by default avoids sending the same GET (or HEAD) request multiple times in parallel.
    • An "HTTP cache" which provides browser-style caching in a (potentially shared) KeyValueCache which observes standard HTTP caching headers.
  • Convenience features such as the ability to specify an un-serialized object as a JSON request body and an easy way to specify URL search parameters
  • Error handling

Documentation

View the Apollo Server documentation for RESTDataSource for more high-level details and examples.

Usage

To get started, install the @apollo/datasource-rest package:

npm install @apollo/datasource-rest

To define a data source, extend the RESTDataSource class and implement the data fetching methods that your resolvers require. Data sources can then be provided via Apollo Server's context object during execution.

Your implementation of these methods can call convenience methods built into the RESTDataSource class to perform HTTP requests, while making it easy to build up query parameters, parse JSON results, and handle errors.

const { RESTDataSource } = require('@apollo/datasource-rest');

class MoviesAPI extends RESTDataSource {
  override baseURL = 'https://movies-api.example.com/';

  async getMovie(id) {
    return this.get(`movies/${encodeURIComponent(id)}`);
  }

  async getMostViewedMovies(limit = 10) {
    const data = await this.get('movies', {
      params: {
        per_page: limit.toString(), // all params entries should be strings
        order_by: 'most_viewed',
      },
    });
    return data.results;
  }
}

API Reference

RESTDataSource is designed to be subclassed in order to create an API for use by the rest of your server. Many of its methods are protected. These consist of HTTP fetching methods (fetch, get, put, post, patch, delete, and head) which your API can call, and other methods that can be overridden to customize behavior.

This README lists all the protected methods. In practice, if you're looking to customize behavior by overriding methods, reading the source code is the best option.

Constructor

The RESTDataSource takes in a DataSourceConfig which allows for overriding some default behavior.

Configuration Overrides

  • cache - Custom KeyValueCache implementation
  • fetch - Custom Fetcher implementation
  • logger - Custom Logger implementation that will replace all logging activity within RESTDataSource

To override the RESTDataSource, see the following example code:

const dataSource = new (class extends RESTDataSource {})({
  cache: customCache,
  fetch: customFetcher,
  logger: customLogger,
});

Properties

baseURL

Optional value to use for all the REST calls. If it is set in your class implementation, this base URL is used as the prefix for all calls. If it is not set, then the value passed to the REST call is exactly the value used. See also resolveURL.

`js title="baseURL.js" class MoviesAPI extends RESTDataSource { override baseURL = 'https://movies-api.example.com/';

// GET async getMovie(id) { return this.get( movies/${encodeURIComponent(id)} // path ); } }


`RESTDataSource` interprets the string passed to methods such as `this.get()` as an URL in exactly the same way that a browser interprets a link on a web page whose address is the same as `this.baseURL`. This may lead to slightly surprising behavior if `this.baseURL` has a non-empty path component:

- If the string passed to a method such as `this.get()` starts with a slash, then it is resolved relative to the *host* of the base URL, not to the full base URL. That is, if `this.baseURL` is `https://foo.com/a/b/c/`, then `this.get('d')` resolves to `https://foo.com/a/b/c/d`, but `this.get('/d')` resolves to `https://foo.com/d`.
- If the base URL has a path element and does not end in a slash, then the given path replaces the last element of the path. That is, if `baseURL` is `https://foo.com/a/b/c`, `this.get('d')` resolves to `https://foo.com/a/b/d`

In practice, this means that you should usually set `this.baseURL` to the common prefix of all URLs you want to access *including a trailing slash*, and you should pass paths *without a leading slash* to methods such as `this.get()`.

If a resource's path starts with something that looks like an URL because it contains a colon and you want it to be added on to the full base URL after its path (so you can't pass it as `this.get('/foo:bar')`), you can pass a path starting with `./`, like `this.get('./foo:bar')`.

##### `httpCache`

This is an internal object that adds HTTP-header-sensitive caching to HTTP fetching. Its exact API is internal to this package and may change between versions.


#### Overridable methods

##### `cacheKeyFor`
By default, `RESTDatasource` uses the `cacheKey` option from the request as the cache key, or the request method and full request URL otherwise when saving information about the request to the `KeyValueCache`. Override this method to remove query parameters or compute a custom cache key.

For example, you could use this to use header fields or the HTTP method as part of the cache key. Even though we do validate header fields and don't serve responses from cache when they don't match, new responses overwrite old ones with different header fields. (For the HTTP method, this might be a positive thing, as you may want a `POST /foo` request to stop a previously cached `GET /foo` from being returned.)

##### `requestDeduplicationPolicyFor`

By default, `RESTDataSource` de-duplicates all **concurrent** outgoing **`GET` (or `HEAD`) requests** in an in-memory cache, separate from the `KeyValueCache` used for the HTTP response cache. It makes the assumption that two `GET` (or two `HEAD`) requests to the same URL made in parallel can share the same response. When the request returns, its response is delivered to each caller that requested the same URL concurrently, and then it is removed from the cache.

If a request is made with the same cache key (method + URL by default) but with an HTTP method other than `GET` or `HEAD`, deduplication of the in-flight request is invalidated: the next parallel `GET` (or `HEAD`) request for the same URL will make a new request.

You can configure this behavior in several ways:
- You can change which requests are de-deduplicated and which are not.
- You can tell `RESTDataSource` to de-duplicate a request against new requests that start after it completes, not just overlapping requests. (This was the poorly-documented behavior of `RESTDataSource` prior to v5.0.0.)
- You can control the "deduplication key" independently from the `KeyValueCache` cache key.

You do this by overriding the `requestDeduplicationPolicyFor` method in your class. This method takes an URL and a request, and returns a policy object with one of three forms:

- `{policy: 'deduplicate-during-request-lifetime', deduplicationKey: string}`: This is the default behavior for `GET` requests. If a request with the same deduplication key is in progress, share its result. Otherwise, start a request, allow other requests to de-duplicate against it while it is running, and forget about it once the request returns successfully.
- `{policy: 'deduplicate-until-invalidated', deduplicationKey: string}`: This was the default behavior for `GET` requests in versions prior to v5. If a request with the same deduplication key is in progress, share its result. Otherwise, start a request and allow other requests to de-duplicate against it while it is running. All future requests with policy `deduplicate-during-request-lifetime` or `deduplicate-until-invalidated` with the same `deduplicationKey` will share the same result until a request is started with policy `do-not-deduplicate` and a matching entry in `invalidateDeduplicationKeys`.
- `{ policy: 'do-not-deduplicate'; invalidateDeduplicationKeys?: string[] }`: This is the default behavior for non-`GET` requests. Always run an actual HTTP request and don't allow other requests to de-duplicate against it. Additionally, invalidate any listed keys immediately: new requests with that `deduplicationKey` will not match any requests that currently exist in the request cache.

The default implementation of this method is:

```ts
protected requestDeduplicationPolicyFor(
  url: URL,
  request: RequestOptions,
): RequestDeduplicationPolicy {
  const method = request.method ?? 'GET';
  // Start with the cache key that is used for the shared header-sensitive
  // cache. Note that its default implementation does not include the HTTP
  // method, so if a subclass overrides this and allows non-GET/HEADs to be
  // de-duplicated it will be important for it to include (at least!) the
  // method in the deduplication key, so we're explicitly adding GET/HEAD here.
  const cacheKey = this.cacheKeyFor(url, request);
  if (['GET', 'HEAD'].includes(method)) {
    return {
      policy: 'deduplicate-during-request-lifetime',
      deduplicationKey: `${method} ${cacheKey}`,
    };
  } else {
    return {
      policy: 'do-not-deduplicate',
      // Always invalidate GETs and HEADs when a different method is seen on the same
      // cache key (ie, URL), as per standard HTTP semantics. (We don't have
      // to invalidate the key with this HTTP method because we never write
      // it.)
      invalidateDeduplicationKeys: [
        this.cacheKeyFor(url, { ...request, method: 'GET' }),
        this.cacheKeyFor(url, { ...request, method: 'HEAD' }),
      ],
    };
  }

To fully disable de-duplication, just always return do-not-duplicate. (This does not affect the HTTP header-sensitive cache.)

class MoviesAPI extends RESTDataSource {
  protected override requestDeduplicationPolicyFor() {
    return { policy: 'do-not-deduplicate' } as const;
  }
}
willSendRequest

This method is invoked at the beginning of processing each request. It's called with the path and request provided to fetch, with a guaranteed non-empty headers and params objects. If a Promise is returned from this method it will wait until the promise is completed to continue executing the request. See the intercepting fetches section for usage examples.

resolveURL

In some cases, you'll want to set the URL based on the environment or other contextual values rather than simply resolving against this.baseURL. To do this, you can override resolveURL:

import type { KeyValueCache } from '@apollo/utils.keyvaluecache';

class PersonalizationAPI extends RESTDataSource {
  override async resolveURL(path: string, _request: AugmentedRequest) {
    if (!this.baseURL) {
      const addresses = await resolveSrv(path.split("/")[1] + ".service.consul");
      this.baseURL = addresses[0];
    }
    return super.resolveURL(path);
  }
}
cacheOptionsFor

Allows setting the CacheOptions to be used for each request/response in the HTTPCache. This is separate from the request-only cache. You can use this to set the TTL to a value in seconds. If you return {ttl: 0}, the response will not be stored. If you return a positive number for ttl and the operation returns a 2xx status code, then the response will be cached, regardless of HTTP headers: make sure this is what you intended! (There is currently no way to say "only cache responses that should be cached according to HTTP headers, but change the TTL to something specific".) Note that if you do not specify ttl here, only GET requests are cached.

You can also specify cacheOptions as part of the "request" in any call to get(), post(), etc. Note that specifically head() calls are not cached at all, so this will have no effect for HEAD requests. This can either be an object such as {ttl: 1}, or a function returning that object. If cacheOptions is provided, cacheOptionsFor is not called (ie, this.cacheOptionsFor is effectively the default value of cacheOptions).

The cacheOptions function and cacheOptionsFor method may be async.

override cacheOptionsFor() {
  return {
    ttl: 1
  }
}
didEncounterError

Note: In previous versions of RESTDataSource (< v5), this hook was expected to throw the error it received (the default implementation did exactly that). This is no longer required; as mentioned below, the error will be thrown immediately after invoking didEncounterError.

You can implement this hook in order to inspect (or modify) errors that are thrown while fetching, parsing the body (parseBody()), or by the throwIfResponseIsError() hook. The error that this hook receives will be thrown immediately after this hook is invoked.

You can also throw a different error here altogether. Note that by default, errors are GraphQLErrors (coming from errorFromResponse).

parseBody

This method is called with the HTTP response and should read the body and parse it into an appropriate format. By default, it checks to see if the Content-Type header starts with application/json or ends with +json (just looking at the header as a string without using a Content-Type parser) and returns response.json() if it does or response.text() if it does not. If you want to read the body in a different way, override this. This method should read the response fully; if it does not, it could cause a memory leak inside the HTTP cache. If you override this, you may want to override cloneParsedBody as well.

cloneParsedBody

This method is used to clone a body (for use by the request deduplication feature so that multiple callers get distinct return values that can be separately mutated). If your parseBody returns values other than basic JSON objects, you might want to override this method too. You can also change this method to return its argument without cloning if your code that uses this class is OK with the values returned from deduplicated requests sharing state.

shouldJSONSerializeBody

By default, this method returns true if the request body is:

  • a plain object or an array
  • an object with a toJSON method (which isn't a Buffer or an instance of a class named FormData)

You can override this method in order to serialize other objects such as custom classes as JSON.

throwIfResponseIsError

After the body is parsed, this method checks a condition (by default, if the HTTP status is 4xx or 5xx) and throws an error created with errorFromResponse if the condition is met.

errorFromResponse

Creates an error based on the response.

catchCacheWritePromiseErrors

This class writes to the shared HTTP-header-sensitive cache in the background (ie, the write is not awaited as part of the HTTP fetch). It passes the Promise associated with that cache write to this method. By default, this method adds a catch handler to the Promise which writes any errors to console.error. You could use this to do different error handling, or to do no error handling if you trust all callers to use the fetch method and await httpCache.cacheWritePromise.

trace

This method wraps the entire processing of a single request; if the NODE_ENV environment variable is equal to development, it logs the request method, URL, and duration. You can override this to provide observability in a different manner.

HTTP Methods

The get method on the RESTDataSource makes an HTTP GET request and returns its parsed body. Similarly, there are methods built-in to allow for POST, PUT, PATCH, DELETE, and HEAD requests. (The head method returns the full FetcherResponse rather than the body because HEAD responses do not have bodies.)

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
    );
  }

  // an example making an HTTP PUT request
  async newMovie(movie) {
    return this.put(
      `movies`, // path
      { body: movie }, // request body
    );
  }

  // an example making an HTTP PATCH request
  async updateMovie(movie) {
    return this.patch(
      `movies`, // path
      { body: { id: movie.id, movie } }, // request body
    );
  }

  // an example making an HTTP DELETE request
  async deleteMovie(movie) {
    return this.delete(
      `movies/${encodeURIComponent(movie.id)}`, // path
    );
  }
}

All of the HTTP helper functions (get, put, post, patch, delete, and head) accept a second parameter for setting the body, headers, params, cacheKey, and cacheOptions (and other Fetch API options).

Alternatively, you can use the fetch method. The return value of this method is a DataSourceFetchResult, which contains parsedBody, response, and some other fields with metadata about how the operation interacted with the cache.

Intercepting fetches

Data sources allow you to intercept fetches to set headers, query parameters, or make other changes to the outgoing request. This is most often used for authorization or other common concerns that apply to all requests. The constructor can be overridden to require additional contextual information when the class is instantiated like so:

class PersonalizationAPI extends RESTDataSource {
  private token: string;

  constructor(token: string) {
    super();
    this.token = token;
  }

  willSendRequest(path, request) {
    // set an authorization header
    request.headers['authorization'] = this.token;
    // or set a query parameter
    request.params.set('api_key', this.token);
  }
}

If you're using TypeScript, you can use the AugmentedRequest type to define the willSendRequest signature:

import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';

class PersonalizationAPI extends RESTDataSource {
  override baseURL = 'https://personalization-api.example.com/';

  private token: string;
  constructor(token: string) {
    super();
    this.token = token;
  }

  override willSendRequest(_path: string, request: AugmentedRequest) {
    request.headers['authorization'] = this.token;
  }
}

Processing Responses

Looking for didReceiveResponse? This section is probably interesting to you.

You might need to read or mutate the response before it's returned. For example, you might need to log a particular header for each request. To do this, you can override the public fetch method like so:

  class MyDataSource extends RESTDataSource {
    override async fetch<TResult>(
      path: string,
      incomingRequest: DataSourceRequest = {}
    ) {
      const result = await super.fetch(path, incomingRequest);
      const header = result.response.headers.get('my-custom-header');
      if (header) {
        console.log(`Found header: ${header}`);
      }
      return result;
    }
  }

This example leverages the default fetch implementation from the parent (super). We append our step to the promise chain, read the header, and return the original result that the super.fetch promise resolved to ({ parsedBody, response }).

Integration with Apollo Server

To give resolvers access to data sources, you create and return them from your context function. (The following example uses the Apollo Server 4 API.)

interface MyContext {
  movies: MoviesAPI;
  personalization: PersonalizationAPI;
}

const server = new ApolloServer<MyContext>({
  typeDefs,
  resolvers,
});

// The context function you provide to your integration should handle constructing your data sources on every request.
const url = await startStandaloneServer(server, {
  async context({ req }) { 
    return {
      moviesAPI: new MoviesAPI(),
      personalizationAPI: new PersonalizationAPI(req.headers['authorization']),
    };
  },
});

From our resolvers, we can access the data source from context and return the result:

const resolvers = {
  Query: {
    movie: async (_source, { id }, { moviesAPI }) => {
      return moviesAPI.getMovie(id);
    },
    mostViewedMovies: async (_source, _args, { moviesAPI }) => {
      return moviesAPI.getMostViewedMovies();
    },
    favorites: async (_source, _args, { personalizationAPI }) => {
      return personalizationAPI.getFavorites();
    },
  },
};

changelog

@apollo/datasource-rest

6.4.1

Patch Changes

6.4.0

Minor Changes

6.3.0

Minor Changes

  • #332 8a4578d Thanks @nmrj! - Allow cache to be skipped on RestDataSource HTTP requests

6.2.2

Patch Changes

  • #270 f6cf377 Thanks @Sean-Y-X! - Use lodash's cloneDeep to clone parsed body instead of JSON.parse(JSON.stringify(...))

  • #268 870ba80 Thanks @HishamAli81! - * Fix RequestOptions.cacheOptions function return type to also return a non-promise value.

    • Fix propagation of the cache options generic type RequestOptions and AugmentedRequest.

6.2.1

Patch Changes

6.2.0

Minor Changes

  • #185 147f820 Thanks @HishamAli81! - Added support to the RESTDatasource to be able to specify a custom cache set options type. The cache set options may need to be customized to include additional set options supported by the underlying key value cache implementation.

    For example, if the InMemoryLRUCache is being used to cache HTTP responses, then noDisposeOnSet, noUpdateTTL, etc cache options can be provided to the LRU cache:

    import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
    
    interface CustomCacheOptions {
      ttl?: number;
      noDisposeOnSet?: boolean;
    }
    
    class ExampleDataSource extends RESTDataSource<CustomCacheOptions> {
      override baseURL = 'https://api.example.com';
    
      constructor() {
        super({ cache: new InMemoryLRUCache() });
      }
    
      getData(id: number) {
        return this.get(`data/${id}`, {
          cacheOptions: { ttl: 3600, noDisposeOnSet: true },
        });
      }
    }

6.1.1

Patch Changes

  • #246 c6ac292 Thanks @lotmek! - Make request and url optional parameters in the errorFromResponse method and clean up the implementation.

6.1.0

Minor Changes

  • #242 dfb8bcc Thanks @trevor-scheer! - Add optional url parameter to didEncounterErrors hook

    In previous versions of RESTDataSource, the URL of the request was available on the Request object passed in to the hook. The Request object is no longer passed as an argument, so this restores the availability of the url to the hook.

    This is optional for now in order to keep this change forward compatible for existing this.didEncounterErrors call sites in userland code. In the next major version, this might become a required parameter.

6.0.1

Patch Changes

  • #214 c7b190a Thanks @trevor-scheer! - Fix bug in Cloudflare Worker usage where we try to call the .raw() method on its response headers object when it doesn't exist.

    For some reason, the Cloudflare Worker's global fetch HeadersList object is passing the instanceof check against node-fetch's Headers class, but it doesn't have the .raw() method we expect on it. To be sure, we can just make sure it's there before we call it.

6.0.0

Major Changes

  • #196 f8f0805 Thanks @trevor-scheer! - Drop Node v14 support

    To take this major version, the only change necessary is to ensure your node runtime is using version 16.14.0 or later.

    Node v14 is EOL, so we should drop support for it and upgrade packages and testing accordingly. Note this package has a dependency on @apollo/utils.keyvaluecache which requires specifically node@>=16.14 due to its dependency on lru-cache.

5.1.1

Patch Changes

5.1.0

Minor Changes

  • #186 5ac9b52 Thanks @js-lowes! - Customize the logger used by RESTDataSource. By default the RESTDataSource will use console. Common use cases would be to override the default logger with pino or winston.

    E.g.

    const pino = require('pino');
    const loggerPino = pino({});
    const dataSource = new (class extends RESTDataSource {})({
      logger: loggerPino,
    });

    In the example above, all logging calls made by the RESTDataSource will now use the pino logger instead of the console logger.

5.0.2

Patch Changes

  • #159 ee018a7 Thanks @trevor-scheer! - Update http-cache-semantics package to latest patch, resolving a security issue.

    Unlike many security updates Apollo repos receive, this is an actual (non-dev) dependency of this package which means it is actually a user-facing security issue.

    The potential impact of this issue is limited to a DOS attack (via an inefficient regex).

    This security issue would only affect you if either:

    • you pass untrusted (i.e. from your users) cache-control request headers
    • you sending requests to untrusted REST server that might return malicious cache-control headers

    Since http-cache-semantics is a careted (^) dependency in this package, the security issue can (and might already) be resolved via a package-lock.json update within your project (possibly triggered by npm audit or another dependency update which has already updated its version of the package in question). If npm ls http-cache-semantics reveals a tree of dependencies which only include the 4.1.1 version (and no references to any previous versions) then you are currently unaffected and this patch should have (for all intents and purpose) no effect.

    More details available here: https://github.com/advisories/GHSA-rc47-6667-2j5j

  • #160 786c44f Thanks @trevor-scheer! - Add missing @apollo/utils.withrequired type dependency which is part of the public typings (via the AugmentedRequest type).

  • #154 bb0cff0 Thanks @JustinSomers! - Addresses duplicate content-type header bug due to upper-cased headers being forwarded. This change instead maps all headers to lowercased headers.

5.0.1

Patch Changes

  • #137 c9ffa7f Thanks @trevor-scheer! - Create intermediate request types (PostRequest, etc.) for consistency and export them. Export DataSourceRequest, DataSourceConfig, and DataSourceFetchResult types.

5.0.0

Version 5 of RESTDataSource addresses many of the long-standing issues and PRs that have existed in this repository (and its former location in the apollo-server repository). While this version does include a number of breaking changes, our hope is that the updated API makes this package more usable and its caching-related behavior less surprising.

The entries below enumerate all of the changes in v5 in detail along with their associated PRs. If you are migrating from v3 or v4, we recommend at least skimming the entries below to see if you're affected by the breaking changes. As always, we recommend using TypeScript with our libraries. This will be especially helpful in surfacing changes to the API which affect your usage. Even if you don't use TypeScript, you can still benefit from the typings we provide using various convenience tools like // @ts-check (with compatible editors like VS Code).

TL;DR

At a higher level, the most notable changes include:

Breaking

  • Remove magic around request deduplication behavior and provide a hook to configure its behavior. Previously, requests were deduplicated forever by default. Now, only requests happening concurrently will be deduplicated (and subsequently cleared from the in-memory cache).
  • Cache keys now include the request method by default (no more overlap in GET and POST requests).
  • Remove the semantically confusing didReceiveResponse hook.
  • Paths now behave as links would in a web browser, allowing path segments to contain colons.

Additive

  • Introduce a public fetch method, giving access to the full Response object
  • Improve ETag header semantics (correctly handle Last-Modified header)
  • Introduce a public head class method for issuing HEAD requests

Major Changes

  • #100 2e51657 Thanks @glasser! - Instead of memoizing GET requests forever in memory, only apply de-duplication during the lifetime of the original request. Replace the memoizeGetRequests field with a requestDeduplicationPolicyFor() method to determine how de-duplication works per request.

    To restore the surprising infinite-unconditional-cache behavior of previous versions, use this implementation of requestDeduplicationPolicyFor() (which replaces deduplicate-during-request-lifetime with deduplicate-until-invalidated):

    override protected requestDeduplicationPolicyFor(
      url: URL,
      request: RequestOptions,
    ): RequestDeduplicationPolicy {
      const cacheKey = this.cacheKeyFor(url, request);
      if (request.method === 'GET') {
        return {
          policy: 'deduplicate-until-invalidated',
          deduplicationKey: `${request.method} ${cacheKey}`,
        };
      } else {
        return {
          policy: 'do-not-deduplicate',
          invalidateDeduplicationKeys: [`GET ${cacheKey}`],
        };
      }
    }

    To restore the behavior of memoizeGetRequests = false, use this implementation of requestDeduplicationPolicyFor():

    protected override requestDeduplicationPolicyFor() {
      return { policy: 'do-not-deduplicate' } as const;
    }
  • #89 4a249ec Thanks @trevor-scheer! - This change restores the full functionality of willSendRequest which previously existed in the v3 version of this package. The v4 change introduced a regression where the incoming request's body was no longer included in the object passed to the willSendRequest hook, it was always undefined.

    For consistency and typings reasons, the path argument is now the first argument to the willSendRequest hook, followed by the AugmentedRequest request object.

  • #115 be4371f Thanks @glasser! - The errorFromResponse method now receives an options object with url, request, response, and parsedBody rather than just a response, and the body has already been parsed.

  • #110 ea43a27 Thanks @trevor-scheer! - Update default cacheKeyFor to include method

    In its previous form, cacheKeyFor only used the URL to calculate the cache key. As a result, when cacheOptions.ttl was specified, the method was ignored. This could lead to surprising behavior where a POST request's response was cached and returned for a GET request (for example).

    The default cacheKeyFor now includes the request method, meaning there will now be distinct cache entries for a given URL per method.

  • #88 2c3dbd0 Thanks @glasser! - When passing params as an object, parameters with undefined values are now skipped, like with JSON.stringify. So you can write:

    getUser(query: string | undefined) {
      return this.get('user', { params: { query } });
    }

    and if query is not provided, the query parameter will be left off of the URL instead of given the value undefined.

    As part of this change, we've removed the ability to provide params in formats other than this kind of object or as an URLSearchParams object. Previously, we allowed every form of input that could be passed to new URLSearchParams(). If you were using one of the other forms (like a pre-serialized URL string or an array of two-element arrays), just pass it directly to new URLSearchParams; note that the feature of stripping undefined values will not occur in this case. For example, you can replace this.get('user', { params: [['query', query]] }) with this.get('user', { params: new URLSearchParams([['query', query]]) }). (URLSearchParams is available in Node as a global.)

  • #107 4b2a6f9 Thanks @trevor-scheer! - Remove didReceiveResponse hook

    The naming of this hook is deceiving; if this hook is overridden it becomes responsible for returning the parsed body and handling errors if they occur. It was originally introduced in https://github.com/apollographql/apollo-server/issues/1324, where the author implemented it due to lack of access to the complete response (headers) in the fetch methods (get, post, ...). This approach isn't a type safe way to accomplish this and places the burden of body parsing and error handling on the user.

    Removing this hook is a prerequisite to a subsequent change that will introduce the ability to fetch a complete response (headers included) aside from the provided fetch methods which only return a body. This change will reinstate the functionality that the author of this hook had originally intended in a more direct manner.

    You reasonably may have used this hook for things like observability and logging, updating response headers, or mutating the response object in some other way. If so, you can now override the public fetch method like so:

    class MyDataSource extends RESTDataSource {
      override async fetch<TResult>(
        path: string,
        incomingRequest: DataSourceRequest = {},
      ) {
        const result = await super.fetch(path, incomingRequest);
        // Log or update here; you have access to `result.parsedBody` and `result.response`.
        // Return the `result` object when you're finished.
        return result;
      }
    }

    All of the convenience http methods (get(), post(), etc.) call this fetch function, so changes here will apply to every request that your datasource makes.

  • #95 c59b82f Thanks @glasser! - Simplify interpretation of this.baseURL so it works exactly like links in a web browser.

    If you set this.baseURL to an URL with a non-empty path component, this may change the URL that your methods talk to. Specifically:

    • Paths passed to methods such as this.get('/foo') now replace the entire URL path from this.baseURL. If you did not intend this, write this.get('foo') instead.
    • If this.baseURL has a non-empty path and does not end in a trailing slash, paths such as this.get('foo') will replace the last component of the URL path instead of adding a new component. If you did not intend this, add a trailing slash to this.baseURL.

    If you preferred the v4 semantics and do not want to make the changes described above, you can restore v4 semantics by overriding resolveURL in your subclass with the following code from v4:

    override resolveURL(path: string): ValueOrPromise<URL> {
      if (path.startsWith('/')) {
        path = path.slice(1);
      }
      const baseURL = this.baseURL;
      if (baseURL) {
        const normalizedBaseURL = baseURL.endsWith('/')
          ? baseURL
          : baseURL.concat('/');
        return new URL(path, normalizedBaseURL);
      } else {
        return new URL(path);
      }
    }

    As part of this change, it is now possible to specify URLs whose first path segment contains a colon, such as this.get('/foo:bar').

  • #121 32f8f04 Thanks @glasser! - We now write to the shared HTTP-header-sensitive cache in the background rather than before the fetch resolves. By default, errors talking to the cache are logged with console.log; override catchCacheWritePromiseErrors to customize. If you call fetch(), the result object has a httpCache.cacheWritePromise field that you can await if you want to know when the cache write ends.

Minor Changes

  • #117 0f94ad9 Thanks @renovate! - If your provided cache is created with PrefixingKeyValueCache.cacheDangerouslyDoesNotNeedPrefixesForIsolation (new in @apollo/utils.keyvaluecache@2.1.0), the httpcache: prefix will not be added to cache keys.

  • #114 6ebc093 Thanks @glasser! - Allow specifying the cache key directly as a cacheKey option in the request options. This is read by the default implementation of cacheKeyFor (which is still called).

  • #106 4cbfd36 Thanks @glasser! - Previously, RESTDataSource doubled the TTL used with its shared header-sensitive cache when it may be able to use the cache entry after it goes stale because it contained the ETag header; for these cache entries, RESTDataSource can set the If-None-Match header when sending the REST request and the server can return a 304 response telling RESTDataSource to reuse the old response from its cache. Now, RESTDataSource also extends the TTL for responses with the Last-Modified header (which it can validate with If-Modified-Since).

  • #110 ea43a27 Thanks @trevor-scheer! - Provide head() HTTP helper method

    Some REST APIs make use of HEAD requests. It seems reasonable for us to provide this method as we do the others.

    It's worth noting that the API differs from the other helpers. While bodies are expected/allowed for other requests, that is explicitly not the case for HEAD requests. This method returns the request object itself rather than a parsed body so that useful information can be extracted from the headers.

  • #114 6ebc093 Thanks @glasser! - Allow specifying the options passed to new CachePolicy() via a httpCacheSemanticsCachePolicyOptions option in the request options.

  • #121 32f8f04 Thanks @glasser! - If you're using node-fetch as your Fetcher implementation (the default) and the response has header names that appear multiple times (such as Set-Cookie), then you can use the node-fetch-specific API (await myRestDataSource.fetch(url)).response.headers.raw() to see the multiple header values separately.

  • #115 be4371f Thanks @glasser! - New throwIfResponseIsError hook allows you to control whether a response should be returned or thrown as an error. Partially replaces the removed didReceiveResponse hook.

  • #116 ac767a7 Thanks @glasser! - The cacheOptions function and cacheOptionsFor method may now optionally be async.

  • #90 b66da37 Thanks @trevor-scheer! - Add a new overridable method shouldJSONSerializeBody for customizing body serialization behavior. This method should return a boolean in order to inform RESTDataSource as to whether or not it should call JSON.stringify on the request body.

  • #110 ea43a27 Thanks @trevor-scheer! - Add public fetch method

    Users previously had no well-defined way to access the complete response (i.e. for header inspection). The public API of HTTP helper methods only returned the parsed response body. A didReceiveResponse hook existed as an attempt to solve this, but its semantics weren't well-defined, nor was it a type safe approach to solving the problem.

    The new fetch method allows users to "bypass" the convenience of the HTTP helpers in order to construct their own full request and inspect the complete response themselves.

    The DataSourceFetchResult type returned by this method also contains other useful information, like a requestDeduplication field containing the request's deduplication policy and whether it was deduplicated against a previous request.

Patch Changes

  • #121 609ba1f Thanks @glasser! - When de-duplicating requests, the returned parsed body is now cloned rather than shared across duplicate requests. If you override the parseBody method, you should also override cloneParsedBody to match.

  • #105 8af22fe Thanks @glasser! - The fetch Response now consistently has a non-empty url property; previously, url was an empty string if the response was read from the HTTP cache.

  • #90 b66da37 Thanks @trevor-scheer! - Correctly identify and serialize all plain objects (like those with a null prototype)

  • #94 834401d Thanks @renovate! - Update @apollo/utils.fetcher dependency to v2.0.0

  • #89 4a249ec Thanks @trevor-scheer! - string and Buffer bodies are now correctly included on the outgoing request. Due to a regression in v4, they were ignored and never sent as the body. string and Buffer bodies are now passed through to the outgoing request (without being JSON stringified).

4.3.2

Patch Changes

4.3.1

Patch Changes

4.3.0

Minor Changes

4.2.0

Minor Changes

  • #5 1857515 Thanks @smyrick! - Rename requestCacheEnabled to memoizeGetRequests. Acknowledging this is actually a breaking change, but this package has been live for a weekend with nothing recommending its usage yet.

4.1.0

Minor Changes

4.0.0

Major Changes