REST Link
Call REST APIs inside your GraphQL queries.
Overview
The Apollo Link Rest library is maintained by Apollo community members and not an Apollo GraphQL maintained library.
Calling REST APIs from a GraphQL client opens the benefits of GraphQL for more people, whether:
You are in a front-end developer team that wants to try GraphQL without asking for the backend team to implement a GraphQL server.
You have no access to change the backend because it's an existing set of APIs, potentially managed by a 3rd party.
You have an existing codebase, but you're looking to evaluate whether GraphQL can work for your needs.
You have a large codebase, and the GraphQL migration is happening on the backend, but you want to use GraphQL now without waiting!
With apollo-link-rest
, you can call your endpoints inside your GraphQL queries and have all your data managed by Apollo Client. apollo-link-rest
is suitable for just dipping your toes in the water, or doing a full-steam ahead integration, and then later on migrating to a backend-driven GraphQL experience.
For more advanced or complex back-ends, you may want to consider using
@apollo/server
.
Quick start
To get started, first install Apollo Client and any peerDependencies
we need:
1npm install --save @apollo/client apollo-link-rest graphql qs
After this, you're ready to setup the Apollo Client instance:
1import { ApolloClient, InMemoryCache } from '@apollo/client';
2import { RestLink } from 'apollo-link-rest';
3
4// Set `RestLink` with your endpoint
5const restLink = new RestLink({ uri: "https://swapi.dev/api/" });
6
7// Setup your client
8const client = new ApolloClient({
9 cache: new InMemoryCache(),
10 link: restLink
11});
Now it's time to write our first query:
1import { gql } from '@apollo/client';
2
3const query = gql`
4 query Luke {
5 person @rest(type: "Person", path: "people/1/") {
6 name
7 }
8 }
9`;
You can then fetch your data using Apollo Client:
1// Invoke the query and log the person's name
2client.query({ query }).then(response => {
3 console.log(response.data.person.name);
4});
Options
The RestLink
constructor accepts an options object that can be used to customize the behavior of the link. Supported options are outlined below:
Option | Type | Description |
---|---|---|
uri | string | The URI key is a string endpoint/domain for your requests to hit (optional when endpoints provides a default) |
endpoints: /map-of-endpoints/ | any | optional A map of endpoints. If you use this, you need to provide endpoint to the @rest(...) directives. |
customFetch? | any | optional A custom fetch to handle REST calls |
headers? | Headers | optional An object representing values to be sent as headers with all requests. Documented here |
credentials? | string | optional A string representing the credentials policy the fetch call should operate with. Document here |
fieldNameNormalizer?: /function/ | any | optional A function that takes the response field name and converts it into a GraphQL compliant name. This is useful if your REST API returns fields that aren't representable as GraphQL, or if you want to convert between snake_case field names in JSON to camelCase keyed fields. |
fieldNameDenormalizer?: /function/ | any | optional A function that takes a GraphQL-compliant field name and converts it back into an endpoint-specific name. |
typePatcher: /map-of-functions/ | any | optional A structure to allow you to specify the __typename when you have nested objects in your REST response. |
defaultSerializer /function/ | any | optional A function that will be used by the RestLink as the default serializer when no bodySerializer is defined for a @rest call. The function will also be passed the current Header set, which can be updated before the request is sent to fetch . Default method uses JSON.stringify and sets the Content-Type to application/json . |
bodySerializers: /map-of-functions/ | any | optional Structure to allow the definition of alternative serializers, which can then be specified by their key. |
responseTransformer?: /function/ | any | optional Apollo expects a record response to return a root object, and a collection of records response to return an array of objects. Use this function to structure the response into the format Apollo expects if your response data is structured differently. |
Multiple endpoints
If you want to be able to use multiple endpoints, you can create your link like:
1const link = new RestLink({ endpoints: { v1: 'api.com/v1', v2: 'api.com/v2' } });
You then need to specify the endpoint you want to use, in the rest directive:
1const postTitleQuery1 = gql`
2 query PostTitle {
3 post @rest(type: "Post", path: "/post", endpoint: "v1") {
4 id
5 title
6 }
7 }
8`;
9const postTitleQuery2 = gql`
10 query PostTitle {
11 post @rest(type: "[Tag]", path: "/tags", endpoint: "v2") {
12 id
13 tags
14 }
15 }
16`;
If you have a default endpoint, you can create your link like:
1const link = new RestLink({
2 endpoints: { github: 'github.com' },
3 uri: 'api.com',
4});
If you don't specify an endpoint in your query, the default endpoint (the one you specify in the uri
option) will be used.
Typename patching
When sending a query like:
1query MyQuery {
2 planets @rest(type: "PlanetPayload", path: "planets/") {
3 count
4 next
5 results {
6 name
7 }
8 }
9}
the outer response object (data.planets
) gets its __typename: "PlanetPayload"
from the @rest(...)
directive's type
parameter. You, however, need to have a way to set the typename of PlanetPayload.results
.
One way you can do this is by providing a typePatcher
:
1const restLink = new RestLink({
2 uri: '/api',
3 typePatcher: {
4 PlanetPayload: (
5 data: any,
6 outerType: string,
7 patchDeeper: RestLink.FunctionalTypePatcher,
8 ): any => {
9 if (data.results != null) {
10 data.results =
11 data.results.map(planet => ({ __typename: "Planet", ...planet }));
12 }
13 return data;
14 },
15 // ... other nested type patchers
16 },
17})
If you have a very lightweight REST integration, you can use the @type(name: ...)
directive.
1query MyQuery {
2 planets @rest(type: "PlanetPayload", path: "planets/") {
3 count
4 next
5 results @type(name: "Planet") {
6 name
7 }
8 }
9}
This is appropriate if you have a small list of nested objects. The cost of this strategy is that every query that deals with these objects needs to also include @type(name: ...)
, which means this approach can be quite verbose and error prone.
You can also use both of these approaches in tandem:
1query MyQuery {
2 planets @rest(type: "PlanetPayload", path: "planets/") {
3 count
4 next
5 results @type(name: "Results") {
6 name
7 }
8 typePatchedResults {
9 name
10 }
11 }
12}
1const restLink = new RestLink({
2 uri: '/api',
3 typePatcher: {
4 PlanetPayload: (
5 data: any,
6 outerType: string,
7 patchDeeper: RestLink.FunctionalTypePatcher,
8 ): any => {
9 if (data.typePatchedResults != null) {
10 data.typePatchedResults =
11 data.typePatchedResults.map(planet => { __typename: "Planet", ...planet });
12 }
13 return data;
14 },
15 // ... other nested type patchers
16 },
17})
Warning
It's important to note that at the moment the typePatcher
is not able to act on nested objects within annotated @type
objects. For instance, failingResults
will not be patched if you define it on the typePatcher
:
1query MyQuery {
2 planets @rest(type: "PlanetPayload", path: "planets/") {
3 count
4 next
5 results @type(name: "Planet") {
6 name
7 failingResults {
8 name
9 }
10 }
11 typePatchedResults {
12 name
13 }
14 }
15}
To make this work you should try to pick one strategy, and stick with it -- either all typePatcher
or all @type
directives.
Response transforming
By default, Apollo expects an object at the root for record requests, and an array of objects at the root for collection requests. For example, if fetching a user by ID (/users/1
), the following response is expected.
1{
2 "id": 1,
3 "name": "Apollo"
4}
And when fetching for a list of users (/users
), the following response is expected.
1[
2 {
3 "id": 1,
4 "name": "Apollo"
5 },
6 {
7 "id": 2,
8 "name": "Starman"
9 }
10]
If the structure of your API responses differs than what Apollo expects, you can define a responseTransformer
in the client. This function receives the response object as the 1st argument, and the current typeName
as the 2nd argument. It should return a Promise
as it will be responsible for reading the response stream by calling one of json()
, text()
etc.
For example, if the record is not at the root level:
1{
2 "meta": {},
3 "data": [
4 {
5 "id": 1,
6 "name": "Apollo"
7 },
8 {
9 "id": 2,
10 "name": "Starman"
11 }
12 ]
13}
The following transformer could be used to support it:
1const link = new RestLink({
2 uri: '/api',
3 responseTransformer: async response => response.json().then(({data}) => data),
4});
Plaintext, XML, or otherwise-encoded responses can be handled by manually parsing and converting them to JSON (using the previously described format that Apollo expects):
1const link = new RestLink({
2 uri: '/xmlApi',
3 responseTransformer: async response => response.text().then(text => parseXmlResponseToJson(text)),
4});
Custom endpoint responses
The client level responseTransformer
applies for all responses, across all URIs and endpoints. If you need a custom responseTransformer
per endpoint, you can define an object of options for that specific endpoint.
1const link = new RestLink({
2 endpoints: {
3 v1: {
4 uri: '/v1',
5 responseTransformer: async response => response.data,
6 },
7 v2: {
8 uri: '/v2',
9 responseTransformer: async (response, typeName) => response[typeName],
10 },
11 },
12});
When using the object form, the
uri
field is required.
Custom Fetch
By default, Apollo uses the browsers fetch
method to handle REST
requests to your domain/endpoint. The customFetch
option allows you to specify your own request handler by defining a function that returns a Promise
with a fetch-response-like object:
1const link = new RestLink({
2 endpoints: "/api",
3 customFetch: (uri, options) => new Promise((resolve, reject) => {
4 // Your own (asynchronous) request handler
5 resolve(responseObject)
6 }),
7});
To resolve your GraphQL queries quickly, Apollo will issue requests to relevant endpoints as soon as possible. This is generally ok, but can lead to large numbers of REST
requests to be fired at once; especially for deeply nested queries (see @export
directive).
Some endpoints (like public APIs) might enforce rate limits, leading to failed responses and unresolved queries in such cases.
By example, customFetch
is a good place to manage your apps fetch operations. The following implementation makes sure to only issue 2 requests at a time (concurrency) while waiting at least 500ms until the next batch of requests is fired.
1import pThrottle from "p-throttle";
2
3const link = new RestLink({
4 endpoints: "/api",
5 customFetch: pThrottle((uri, config) => {
6 return fetch(uri, config);
7 },
8 2, // Max. concurrent Requests
9 500 // Min. delay between calls
10 ),
11});
Since Apollo issues
Promise
based requests, we can resolve them as we see fit. This example usespThrottle
; part of the popular promise-fun collection.
Complete options
Here is one way you might customize RestLink
:
1import fetch from 'cross-fetch';
2import * as camelCase from 'camelcase';
3import * as snake_case from 'snake-case';
4
5const link = new RestLink({
6 endpoints: { github: 'github.com' },
7 uri: 'api.com',
8 customFetch: fetch,
9 headers: {
10 "Content-Type": "application/json"
11 },
12 credentials: "same-origin",
13 fieldNameNormalizer: (key: string) => camelCase(key),
14 fieldNameDenormalizer: (key: string) => snake_case(key),
15 typePatcher: {
16 Post: ()=> {
17 bodySnippet...
18 }
19 },
20 defaultSerializer: (data: any, headers: Headers) => {
21 const formData = new FormData();
22 for (let key in data) {
23 formData.append(key, data[key]);
24 }
25 headers.set("Content-Type", "x-www-form-encoded")
26 return {data: formData, headers};
27 }
28});
Link Context
RestLink
has an interface LinkChainContext
which it uses as the structure of things that it will look for in the context
, as it decides how to fulfill a specific RestLink
request. (Please see the @apollo/client/link/context
page for a discussion of why you might want this).
Option | Type | Description |
---|---|---|
credentials? | RequestCredentials | Overrides the RestLink -level setting for credentials . Values documented here |
headers? | Headers | Additional headers provided in this context-link Values documented here |
headersToOverride? | string[] | If you provide this array, we will merge the headers you provide in this link, by replacing any matching headers that exist in the root RestLink configuration. Alternatively you can use headersMergePolicy for more fine-grained customization of the merging behavior. |
headersMergePolicy? | RestLink.HeadersMergePolicy | This is a function that decide how the headers returned in this contextLink are merged with headers defined at the RestLink -level. If you don't provide this, the headers will be simply appended. To use this option, you can provide your own function that decides how to process the headers. Code references |
restResponses? | Response[] | This will be populated after the operation has completed with the Responses of every REST url fetched during the operation. This can be useful if you need to access the response headers to grab an authorization token for example. |
Example
RestLink
uses the headers
field on the @apollo/client/link/context
so you can compose other links that provide additional & dynamic headers to a given query.
Here is one way to add request headers
to the context and retrieve the response headers of the operation:
1const authRestLink = new ApolloLink((operation, forward) => {
2 operation.setContext(({headers}) => {
3 const token = localStorage.getItem("token");
4 return {
5 headers: {
6 ...headers,
7 Accept: "application/json",
8 Authorization: token
9 }
10 };
11 });
12 return forward(operation).map(result => {
13 const { restResponses } = operation.getContext();
14 const authTokenResponse = restResponses.find(res => res.headers.has("Authorization"));
15 // You might also filter on res.url to find the response of a specific API call
16 if (authTokenResponse) {
17 localStorage.setItem("token", authTokenResponse.headers.get("Authorization"));
18 }
19 return result;
20 });
21});
22
23const restLink = new RestLink({ uri: "uri" });
24
25const client = new ApolloClient({
26 cache: new InMemoryCache(),
27 link: ApolloLink.from([authRestLink, restLink])
28});
Link order
If you are using multiple link types, restLink
should go before httpLink
, as httpLink
will swallow any calls that should be routed through apollo-link-rest
.
For example:
1const httpLink = createHttpLink({ uri: "server.com/graphql" });
2const restLink = new RestLink({ uri: "api.server.com" });
3
4const client = new ApolloClient({
5 cache: new InMemoryCache(),
6 link: ApolloLink.from([authLink, restLink, errorLink, retryLink, httpLink])
7 // Note: httpLink is terminating so must be last, while retry & error wrap
8 // the links to their right. State & context links should happen before (to
9 // the left of) restLink.
10});
Note: you should also consider this if you're using @apollo/client/link/context
to set Headers
, you need that link to be before restLink
as well.
@rest directive
This is where you setup the endpoint you want to fetch. The rest directive can be used at any depth in a query.
Arguments
An @rest(…)
directive takes two required and several optional arguments:
Option | Type | Description |
---|---|---|
type | string | The GraphQL type this will return |
path | string | uri-path to the REST API. This could be a path or a full url. If a path, the endpoint given on link creation or from the context is concatenated with it to produce a full URI . See also: pathBuilder |
method? | GET PUT POST DELETE | The HTTP method to send the request via (i.e GET, PUT, POST) |
endpoint? | string | Key to use when looking up the endpoint in the (optional) endpoints table if provided to RestLink at creation time. |
pathBuilder?: /function/ | string | If provided, this function gets to control what path is produced for this request. |
bodyKey?: "input" | string | This is the name of the variable to use when looking to build a REST request-body for a PUT or POST request. It defaults to input if not supplied. |
bodyBuilder?: /function/ | string | If provided, this is the name a function that you provided to variables , that is called when a request-body needs to be built. This lets you combine arguments or encode the body in some format other than JSON. |
bodySerializer?: /string | function/ | string | String key to look up a function in bodySerializers or a custom serialization function for the body/headers of this request before it is passed to the fetch call. Defaults to JSON.stringify and setting Content-Type: application-json . |
Variables
You can use query variables
inside nested queries, or in the the path argument of your directive:
1query PostTitle {
2 post(id: "1") @rest(type: "Post", path: "/post/{args.id}") {
3 id
4 title
5 }
6}
Warning: Variables in the main path will not automatically have
encodeURIComponent
called on them.
Additionally, you can also control the query-string:
1query PostTitle {
2 postSearch(query: "some key words", page_size: 5)
3 @rest(type: "Post", path: "/search?{args}&{context.language}") {
4 id
5 title
6 }
7}
Things to note:
This will be converted into
/search?query=some%20key%20words&page_size=5&lang=en
The
context.language / lang=en
is extracting an object from the Apollo Context, that was added via an@apollo/client/link/context
Link.The query string arguments are assembled by npm:qs and have
encodeURIComponent
called on them.
The available variable sources are:
Option | Description |
---|---|
args | These are the things passed directly to this field parameters. In the above example postSearch had query and page_size in args. |
exportVariables | These are the things in the parent context that were tagged as @export(as: ...) |
context | These are the apollo-context, so you can have globals set up via @apollo/client/link/context |
@rest | These include any other parameters you pass to the @rest() directive. This is probably more useful when working with pathBuilder , documented below. |
pathBuilder
If the variable-replacement options described above aren't enough, you can provide a pathBuilder
to your query. This will be called to dynamically construct the path. This is considered an advanced feature, and is documented in the source -- it also should be considered syntactically unstable, and we're looking for feedback!
bodyKey
/ bodyBuilder
When making a POST
or PUT
HTTP request, you often need to provide a request body. By convention, GraphQL recommends you name your input-types as input
, so by default that's where we'll look to find a JSON object for your body.
bodyKey
If you need/want to name it something different, you can pass bodyKey
, and we'll look at that variable instead.
In this example the publish API accepts a body in the variable body
instead of input:
1mutation PublishPost(
2 $someApiWithACustomBodyKey: PublishablePostInput!
3) {
4 publishedPost: publish(input: "Foo", body: $someApiWithACustomBodyKey)
5 @rest(
6 type: "Post"
7 path: "/posts/{args.input}/new"
8 method: "POST"
9 bodyKey: "body"
10 ) {
11 id
12 title
13 }
14}
bodyBuilder
If you need to structure your data differently, or you need to custom encode your body (say as form-encoded), you can provide bodyBuilder
instead:
1mutation EncryptedPost(
2 $input: PublishablePostInput!
3 $encryptor: any
4) {
5 publishedPost: publish(input: $input)
6 @rest(
7 type: "Post"
8 path: "/posts/new"
9 method: "POST"
10 bodyBuilder: $encryptor
11 ) {
12 id
13 title
14 }
15}
bodySerializer
If you need to serialize your data differently (say as form-encoded), you can provide a bodySerializer
instead of relying on the default JSON serialization.
bodySerializer
can be either a function of the form (data: any, headers: Headers) => {body: any, header: Headers}
or a string key. When using the string key
RestLink
will instead use the corresponding serializer from the bodySerializers
object, that can optionally be passed in during initialization.
1mutation EncryptedForm(
2 $input: PublishablePostInput!,
3 $formSerializer: any
4) {
5 publishedPost: publish(input: $input)
6 @rest(
7 type: "Post",
8 path: "/posts/new",
9 method: "POST",
10 bodySerializer: $formSerializer
11 ) {
12 id
13 title
14 }
15
16 publishRSS(input: $input)
17 @rest(
18 type: "Post",
19 path: "/feed",
20 method: "POST",
21 bodySerializer: "xml"
22 )
23}
Where formSerializer
could be defined as
1const formSerializer = (data: any, headers: Headers) => {
2 const formData = new FormData();
3 for (let key in data) {
4 if (data.hasOwnProperty(key)) {
5 formData.append(key, data[key]);
6 }
7 }
8
9 headers.set('Content-Type', 'application/x-www-form-urlencoded');
10
11 return {body: formData, headers};
12}
13
And "xml"
would have been defined on the RestLink
directly
1const restLink = new RestLink({
2 ...otherOptions,
3 bodySerializers: {
4 xml: xmlSerializer
5 }
6})
@export directive
The export directive re-exposes a field for use in a later (nested) query. These are the same semantics that will be supported on the server, but when used in a RestLink
you can use the exported variables for further calls (i.e. waterfall requests from nested fields).
Note: If you're constantly using @export you may prefer to take a look at @apollo/server
.
Arguments
as: string
: name to create this as a variable to be used down the selection set
Example
An example use-case would be getting a list of users, and hitting a different endpoint to fetch more data using the exported field in the REST query args.
1const QUERY = gql`
2 query RestData($email: String!) {
3 users @rest(path: '/users/email?{args.email}', method: 'GET', type: 'User') {
4 id @export(as: "id")
5 firstName
6 lastName
7 friends @rest(path: '/friends/{exportVariables.id}', type: '[User]') {
8 firstName
9 lastName
10 }
11 }
12 }
13`;
Mutations
You can write also mutations with the apollo-link-rest, for example:
1mutation DeletePost($id: ID!) {
2 deletePostResponse(id: $id)
3 @rest(type: "Post", path: "/posts/{args.id}", method: "DELETE") {
4 NoResponse
5 }
6}
Troubleshooting
Here are a few common apollo-link-rest
problems and solutions.
Missing field __typename in ...
-- If you see this, it's possible you haven't providedtype:
to the@rest(...)
-directive. Alternately you need to set up atypePatcher
.Headers is undefined
-- If you see something like this, you're running in a browser or other Javascript environment that does not yet support the full specification for theHeaders
API.
Example apps
To get you started, here are some example apps:
Simple: A very simple app with a single query that reflects the setup section.
Advanced: A more complex app that demonstrates how to use an export directive.
Contributing
Please join us on github apollographql/apollo-link-rest and in the Apollo GraphQL community forums.