Sometimes, when your application is in the middle of the migration from REST to GraphQL API, you might find yourself in the situation where data your need is split between both APIs. Let's say when you were fetching data from REST API, you were storing it in your application global state - be it Redux, MobX or Vuex. But with the new shiny GraphQL API you don't even need to bother about creating a boilerplate for storing the response - Apollo Client will take care of this process for you! Does it mean with two APIs you need to stick to the old good boring solution and ditch Apollo Client cache? Not at all!
You can wrap your REST API calls with Apollo and store results in Apollo cache too. If you have a large application and have many of them, you can use an apollo-link-rest library for this. In this article, we will create a basic DIY approach to this task to understand better how Apollo resolvers work and how we can use them in our application for good.
What we're going to build?
As an example, we will use a Vue single-page application built on top of Rick and Morty API. The good thing about this API is it has both REST and GraphQL endpoints, so we can experiment with it a bit.
Let's imagine our application was using REST API exclusively. So, on the frontend side, we had a Vuex store and we called axios
queries from Vuex actions to fetch characters and episodes from the API.
// Vuex state
state: {
episodes: [],
characters: [],
favoriteCharacters: [],
isLoading: false,
error: null
},
// Vuex actions
actions: {
getEpisodes({ commit }) {
commit('toggleLoading', true);
axios
.get('/episode')
.then(res => commit('setEpisodes', res.data.results))
.catch(err => commit('setError', error))
.finally(() => commit('toggleLoading', false));
},
getCharacters({ commit }) {
commit('toggleLoading', true);
axios
.get('/character')
.then(res => commit('setCharacters', res.data.results))
.catch(err => commit('setError', err))
.finally(() => commit('toggleLoading', false));
},
addToFavorites({ commit }, character) {
commit('addToFavorites', character);
},
removeFromFavorites({ commit }, characterId) {
commit('removeFromFavorites', characterId);
}
}
I don't list Vuex mutations here as they're pretty much intuitive - we assign fetched characters to state.characters
etc.
As you can see, we needed to handle the loading flag manually as well as storing an error if something went wrong.
Every single character in characters
array is an object:
Now let's imagine our backend developers created a query for us to fetch episodes, but characters still need to be fetched via REST API. So, how we can handle this?
Step 1: extend GraphQL schema
In GraphQL, anything we can fetch from the endpoint, must have a type and be defined in GraphQL schema. Let's be consistent and add characters
to schema too. 'But how?' - you might ask, 'schema is defined on the backend!'. That's true but we can extend this schema on the frontend too! This process is called schema stitching
. While this step is completely optional, I would still recommend always define GraphQL type definitions for your entities even if they are local. It helps you if you use a code generation to create e.g. TypeScript types from the GraphQL schema and also it enables validation and auto-completion if you use an Apollo plugin in your IDE.
Let's create a new type for characters. We will be using graphql-tag
to parse the string to GraphQL type:
// client.js
import gql from "graphql-tag";
const typeDefs = gql`
type Character {
id: ID!
name: String
location: String
image: String
}
`;
As you can see, here we don't use all the fields from the character
object, only those we need.
Now we also need to extend a Query
type with the GraphQL characters
query:
// client.js
import gql from "graphql-tag";
const typeDefs = gql`
type Character {
id: ID!
name: String
location: String
image: String
}
extend type Query {
characters: [Character]
}
`;
To stitch this part of the schema with the schema fetched from the GraphQL endpoint, we need to pass typeDefs
to the GraphQL client options:
// client.js
import { ApolloClient } from "apollo-client";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import gql from "graphql-tag";
const httpLink = createHttpLink({
uri: "https://rickandmortyapi.com/graphql"
});
const cache = new InMemoryCache();
const typeDefs = gql`
type Character {
id: ID!
name: String
location: String
image: String
}
extend type Query {
characters: [Character]
}
`;
export const apolloClient = new ApolloClient({
link: httpLink,
cache,
typeDefs
});
Step 2: Writing a query and a resolver
We need to define a GraphQL query with a @client
directive to be called when we want to fetch characters. @client
directive tells Apollo Client not to fetch this data from the GraphQL endpoint but the local cache. Usually, I keep queries in .gql
files and add a graphql-tag/loader
to webpack configuration to be able to import them.
// characters.query.gql
query Characters {
characters @client {
id
name
location
image
}
}
But there is one issue: there are no characters in the local cache! How do we 'explain' to Apollo Client where it can get this data? For these purposes, we need to write a resolver. This resolver will be called every time we try to fetch characters to render them in our application.
Let's create a resolvers object and define a resolver for characters
query
// client.js
const resolvers = {
Query: {
characters() {
...
}
}
};
What should we do here? Well, we need to perform the same axios call we did in Vuex action! We will map response fields to our GraphQL type fields to make a structure plainer:
// client.js
const resolvers = {
Query: {
characters() {
return axios.get("/character").then(res =>
res.data.results.map(char => ({
__typename: "Character",
id: char.id,
name: char.name,
location: char.location.name,
image: char.image
}))
);
}
}
};
That's it! Now, when we call GraphQL characters
query, our resolver will perform a REST API call and return a result for us. Bonus point: $apollo.queries.characters.loading
property will change accordingly when the REST API call is in progress! Also, if some error happens on this call. the Apollo query error
hook will be triggered.
Conclusion
As you can see, having a part of the API on the REST endpoint doesn't prevent you from using Apollo Client and its cache. Any REST API call could be wrapped with Apollo resolver and its result can be stored to the Apollo cache which can simplify the migration process.
Top comments (3)
Finally. You are the first person that lift the local resolvers conundrum for me. I have read tech.trello.com/adopting-graphql-a... and then apollographql.com/docs/react/local... but I don't use react and I did not know what the equivalent for local state was. And now, local resolvers are deprecated ¬.¬
Can you maybe update this article or make a new one with apollographql.com/docs/react/local... ?
Apollo Client v3 deprecated the client resolvers
Do we need register const resolvers ={...} to ApolloClient ?
*Like typeDefs, right ?