Docs
Launch GraphOS Studio

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 maintained library.

Calling REST APIs from a opens the benefits of GraphQL for more people, whether:

  • You are in a front-end developer team that wants to try without asking for the backend team to implement a .
  • 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 can work for your needs.
  • You have a large codebase, and the 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 queries and have all your data managed by . 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 experience.

For more advanced or complex back-ends, you may want to consider using @apollo/server.

Quick start

To get started, first install and any peerDependencies we need:

npm install --save @apollo/client apollo-link-rest graphql qs

After this, you're ready to setup the instance:

import { ApolloClient, InMemoryCache } from '@apollo/client';
import { RestLink } from 'apollo-link-rest';
// Set `RestLink` with your endpoint
const restLink = new RestLink({ uri: "https://swapi.dev/api/" });
// Setup your client
const client = new ApolloClient({
cache: new InMemoryCache(),
link: restLink
});

Now it's time to write our first :

import { gql } from '@apollo/client';
const query = gql`
query Luke {
person @rest(type: "Person", path: "people/1/") {
name
}
}
`;

You can then fetch your data using :

// Invoke the query and log the person's name
client.query({ query }).then(response => {
console.log(response.data.person.name);
});

Options

The RestLink constructor accepts an options object that can be used to customize the behavior of the link. Supported options are outlined below:

OptionTypeDescription
uristringThe URI key is a string endpoint/domain for your requests to hit (optional when endpoints provides a default)
endpoints: /map-of-endpoints/anyoptional A map of endpoints. If you use this, you need to provide endpoint to the @rest(...) directives.
customFetch?anyoptional A custom fetch to handle REST calls
headers?Headersoptional An object representing values to be sent as headers with all requests. Documented here
credentials?stringoptional A string representing the credentials policy the fetch call should operate with. Document here
fieldNameNormalizer?: /function/anyoptional 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/anyoptional A function that takes a GraphQL-compliant field name and converts it back into an endpoint-specific name.
typePatcher: /map-of-functions/anyoptional A structure to allow you to specify the __typename when you have nested objects in your REST response.
defaultSerializer /function/anyoptional 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/anyoptional Structure to allow the definition of alternative serializers, which can then be specified by their key.
responseTransformer?: /function/anyoptional 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:

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

const postTitleQuery1 = gql`
query PostTitle {
post @rest(type: "Post", path: "/post", endpoint: "v1") {
id
title
}
}
`;
const postTitleQuery2 = gql`
query PostTitle {
post @rest(type: "[Tag]", path: "/tags", endpoint: "v2") {
id
tags
}
}
`;

If you have a default endpoint, you can create your link like:

const link = new RestLink({
endpoints: { github: 'github.com' },
uri: 'api.com',
});

If you don't specify an endpoint in your , the default endpoint (the one you specify in the uri option) will be used.

Typename patching

When sending a like:

query MyQuery {
planets @rest(type: "PlanetPayload", path: "planets/") {
count
next
results {
name
}
}
}

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:

const restLink = new RestLink({
uri: '/api',
typePatcher: {
PlanetPayload: (
data: any,
outerType: string,
patchDeeper: RestLink.FunctionalTypePatcher,
): any => {
if (data.results != null) {
data.results =
data.results.map(planet => ({ __typename: "Planet", ...planet }));
}
return data;
},
// ... other nested type patchers
},
})

If you have a very lightweight REST integration, you can use the @type(name: ...) .

query MyQuery {
planets @rest(type: "PlanetPayload", path: "planets/") {
count
next
results @type(name: "Planet") {
name
}
}
}

This is appropriate if you have a small list of nested objects. The cost of this strategy is that every 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:

query MyQuery {
planets @rest(type: "PlanetPayload", path: "planets/") {
count
next
results @type(name: "Results") {
name
}
typePatchedResults {
name
}
}
}
const restLink = new RestLink({
uri: '/api',
typePatcher: {
PlanetPayload: (
data: any,
outerType: string,
patchDeeper: RestLink.FunctionalTypePatcher,
): any => {
if (data.typePatchedResults != null) {
data.typePatchedResults =
data.typePatchedResults.map(planet => { __typename: "Planet", ...planet });
}
return data;
},
// ... other nested type patchers
},
})

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:

query MyQuery {
planets @rest(type: "PlanetPayload", path: "planets/") {
count
next
results @type(name: "Planet") {
name
failingResults {
name
}
}
typePatchedResults {
name
}
}
}

To make this work you should try to pick one strategy, and stick with it -- either all typePatcher or all @type .

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.

{
"id": 1,
"name": "Apollo"
}

And when fetching for a list of users (/users), the following response is expected.

[
{
"id": 1,
"name": "Apollo"
},
{
"id": 2,
"name": "Starman"
}
]

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 , and the current typeName as the 2nd . 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:

{
"meta": {},
"data": [
{
"id": 1,
"name": "Apollo"
},
{
"id": 2,
"name": "Starman"
}
]
}

The following transformer could be used to support it:

const link = new RestLink({
uri: '/api',
responseTransformer: async response => response.json().then(({data}) => data),
});

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

const link = new RestLink({
uri: '/xmlApi',
responseTransformer: async response => response.text().then(text => parseXmlResponseToJson(text)),
});

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.

const link = new RestLink({
endpoints: {
v1: {
uri: '/v1',
responseTransformer: async response => response.data,
},
v2: {
uri: '/v2',
responseTransformer: async (response, typeName) => response[typeName],
},
},
});

When using the object form, the uri 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:

const link = new RestLink({
endpoints: "/api",
customFetch: (uri, options) => new Promise((resolve, reject) => {
// Your own (asynchronous) request handler
resolve(responseObject)
}),
});

To resolve your 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 . 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.

import pThrottle from "p-throttle";
const link = new RestLink({
endpoints: "/api",
customFetch: pThrottle((uri, config) => {
return fetch(uri, config);
},
2, // Max. concurrent Requests
500 // Min. delay between calls
),
});

Since Apollo issues Promise based requests, we can resolve them as we see fit. This example uses pThrottle; part of the popular promise-fun collection.

Complete options

Here is one way you might customize RestLink:

import fetch from 'cross-fetch';
import * as camelCase from 'camelcase';
import * as snake_case from 'snake-case';
const link = new RestLink({
endpoints: { github: 'github.com' },
uri: 'api.com',
customFetch: fetch,
headers: {
"Content-Type": "application/json"
},
credentials: "same-origin",
fieldNameNormalizer: (key: string) => camelCase(key),
fieldNameDenormalizer: (key: string) => snake_case(key),
typePatcher: {
Post: ()=> {
bodySnippet...
}
},
defaultSerializer: (data: any, headers: Headers) => {
const formData = new FormData();
for (let key in data) {
formData.append(key, data[key]);
}
headers.set("Content-Type", "x-www-form-encoded")
return {data: formData, headers};
}
});

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).

OptionTypeDescription
credentials?RequestCredentialsOverrides the RestLink-level setting for credentials. Values documented here
headers?HeadersAdditional headers provided in this context-linkValues 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.HeadersMergePolicyThis 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 on the @apollo/client/link/context so you can compose other links that provide additional & dynamic headers to a given .

Here is one way to add request headers to the context and retrieve the response headers of the :

const authRestLink = new ApolloLink((operation, forward) => {
operation.setContext(({headers}) => {
const token = localStorage.getItem("token");
return {
headers: {
...headers,
Accept: "application/json",
Authorization: token
}
};
});
return forward(operation).map(result => {
const { restResponses } = operation.getContext();
const authTokenResponse = restResponses.find(res => res.headers.has("Authorization"));
// You might also filter on res.url to find the response of a specific API call
if (authTokenResponse) {
localStorage.setItem("token", authTokenResponse.headers.get("Authorization"));
}
return result;
});
});
const restLink = new RestLink({ uri: "uri" });
const client = new ApolloClient({
cache: new InMemoryCache(),
link: ApolloLink.from([authRestLink, restLink])
});

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:

const httpLink = createHttpLink({ uri: "server.com/graphql" });
const restLink = new RestLink({ uri: "api.server.com" });
const client = new ApolloClient({
cache: new InMemoryCache(),
link: ApolloLink.from([authLink, restLink, errorLink, retryLink, httpLink])
// Note: httpLink is terminating so must be last, while retry & error wrap
// the links to their right. State & context links should happen before (to
// the left of) restLink.
});

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 can be used at any depth in a .

Arguments

An @rest(…) takes two required and several optional :

OptionTypeDescription
typestringThe GraphQL type this will return
pathstringuri-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?GETPUTPOSTDELETEThe HTTP method to send the request via (i.e GET, PUT, POST)
endpoint?stringKey to use when looking up the endpoint in the (optional) endpoints table if provided to RestLink at creation time.
pathBuilder?: /function/stringIf provided, this function gets to control what path is produced for this request.
bodyKey?: "input"stringThis 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/stringIf 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/stringString 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 variables inside nested queries, or in the the path of your :

query PostTitle {
post(id: "1") @rest(type: "Post", path: "/post/{args.id}") {
id
title
}
}

Warning: in the main path will not automatically have encodeURIComponent called on them.

Additionally, you can also control the -string:

query PostTitle {
postSearch(query: "some key words", page_size: 5)
@rest(type: "Post", path: "/search?{args}&{context.language}") {
id
title
}
}

Things to note:

  1. This will be converted into /search?query=some%20key%20words&page_size=5&lang=en
  2. The context.language / lang=en is extracting an object from the Apollo Context, that was added via an @apollo/client/link/context Link.
  3. The string are assembled by npm:qs and have encodeURIComponent called on them.

The available sources are:

OptionDescription
argsThese are the things passed directly to this field parameters. In the above example postSearch had query and page_size in args.
exportVariablesThese are the things in the parent context that were tagged as @export(as: ...)
contextThese are the apollo-context, so you can have globals set up via @apollo/client/link/context
@restThese include any other parameters you pass to the @rest() directive. This is probably more useful when working with pathBuilder, documented below.

pathBuilder

If the -replacement options described above aren't enough, you can provide a pathBuilder to your . This will be called to dynamically construct the path. This is considered an advanced feature, and is 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, 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 instead.

In this example the publish API accepts a body in the body instead of input:

mutation PublishPost(
$someApiWithACustomBodyKey: PublishablePostInput!
) {
publishedPost: publish(input: "Foo", body: $someApiWithACustomBodyKey)
@rest(
type: "Post"
path: "/posts/{args.input}/new"
method: "POST"
bodyKey: "body"
) {
id
title
}
}

Unit Test

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:

mutation EncryptedPost(
$input: PublishablePostInput!
$encryptor: any
) {
publishedPost: publish(input: $input)
@rest(
type: "Post"
path: "/posts/new"
method: "POST"
bodyBuilder: $encryptor
) {
id
title
}
}

Unit Test

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.

mutation EncryptedForm(
$input: PublishablePostInput!,
$formSerializer: any
) {
publishedPost: publish(input: $input)
@rest(
type: "Post",
path: "/posts/new",
method: "POST",
bodySerializer: $formSerializer
) {
id
title
}
publishRSS(input: $input)
@rest(
type: "Post",
path: "/feed",
method: "POST",
bodySerializer: "xml"
)
}

Where formSerializer could be defined as

const formSerializer = (data: any, headers: Headers) => {
const formData = new FormData();
for (let key in data) {
if (data.hasOwnProperty(key)) {
formData.append(key, data[key]);
}
}
headers.set('Content-Type', 'application/x-www-form-urlencoded');
return {body: formData, headers};
}

And "xml" would have been defined on the RestLink directly

const restLink = new RestLink({
...otherOptions,
bodySerializers: {
xml: xmlSerializer
}
})

@export directive

The export re-exposes a for use in a later (nested) . These are the same semantics that will be supported on the server, but when used in a RestLink you can use the exported for further calls (i.e. waterfall requests from nested ).

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 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 in the REST args.

const QUERY = gql`
query RestData($email: String!) {
users @rest(path: '/users/email?{args.email}', method: 'GET', type: 'User') {
id @export(as: "id")
firstName
lastName
friends @rest(path: '/friends/{exportVariables.id}', type: '[User]') {
firstName
lastName
}
}
}
`;

Mutations

You can write also with the apollo-link-rest, for example:

mutation DeletePost($id: ID!) {
deletePostResponse(id: $id)
@rest(type: "Post", path: "/posts/{args.id}", method: "DELETE") {
NoResponse
}
}

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 provided type: to the @rest(...)-. Alternately you need to set up a typePatcher.
  • 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 the Headers API.

Example apps

To get you started, here are some example apps:

  • Simple: A very simple app with a single that reflects the setup section.
  • Advanced: A more complex app that demonstrates how to use an export .

Contributing

Please join us on github apollographql/apollo-link-rest and in the Apollo GraphQL community forums.

Previous
Remove Typename
Next
Retry
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company