Frontend developers spend a lot of time integrating their components and pages with the backend API. Sometimes, they can’t continue working on a specific feature until it’s implemented on the backend. And other times, they can’t even load the app because the backend API might be down or they don’t have an internet connection. Or maybe they have a slow internet connection, which would make the development workflow slower.
This wouldn’t be an issue if the frontend developers have access to the backend project locally, but sometimes they don’t.
One good solution to this problem is mocking the GraphQL data on the frontend side.
In this article, I’ll show you how to set that up for GraphQL Apollo.
We need to create a new Apollo client
When building frontend apps, we usually instantiate an Apollo client, give it the GraphQL server url, configure it, and then start using it.
We still need this, but also we need to create another instance for the mocked data. However, instead of giving it the GraphQL server url, we give it a schema.
Only one client should be used when running the app – we can use an env variable to decide which one to use.
function createApolloClient() {
if (env.USE_MOCK) {
return createMockedClient()
}
return createLiveClient()
}
function createLiveClient() {
// Creates and returns an Apollo client based on a GraphQL server URL
const apolloClient = new ApolloClient({
uri: 'http://yourgraphqlserver'
})
}
createMockedClient() {
// Creates and returns an Apollo client based on a provided schema
// I will show the code later
}
In this article, we are interested in createMockedClient()
.
createLiveClient()
should be what you usually write when creating an Apollo client with a GraphQL server url.
Defining the schemas
As mentioned above, we have to provide the schema code directly to the mocked client. But there are two cases when mocking the data. It’s either mocking operations (queries or mutations) that don’t exist on the backend yet, or mocking operations that already exist on the backend.
The first case would be when we want to start implementing a feature on the frontend without waiting for the backend to be ready. For this case we need to define the schema on the frontend, but we need to be sure we are using the same schema as what the backend will implement.
The other case is useful when we want to work on the frontend without being affected by the internet connection. In this case, the schema has to be the same as the live one. We can easily get the same schema from the backend by downloading it with the help of some tools, like graphql-cli or get-graphql-schema.
To make it easy to distinguish between the existing schema and the new one, I like to store each one in a different file – schema.js
for the one downloaded from the server, and nextSchema.js
for the one defined on the frontend only.
These files will look something like this:
// schema.js
export const schema = gql`
// Existing types and inputs
type Item {}
// Exisiting queries and mutations
type Query {}
type Mutation {}
`
// nextSchema.js
export const schema = gql`
// New types and inputs
type NewItem {}
// New queries and mutations
type Query {}
type Mutation {}
`
createMockedClient()
Let me first show you an example of how an Apollo client with mocked data can be created, and then I’ll explain it below.
But first make sure to install the needed NPM packages:
npm install @graphql-tools/schema @graphql-tools/mock @faker-js/faker
import { ApolloClient, InMemoryCache } from '@apollo/client/core'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { addMocksToSchema } from '@graphql-tools/mock'
import { SchemaLink } from '@apollo/client/link/schema'
import { faker } from '@faker-js/faker'
import { schema } from './schema.js'
import { nextSchema } from './nextSchema.js'
function createMockedClient() {
const executableSchema = makeExecutableSchema({
typeDefs: [schema, nextSchema]
})
const schemaWithMocks = addMocksToSchema({
executableSchema,
mocks: {
Query: () => ({
items: [...new Array(5)]
}),
Item: () => ({
id: faker.datatype.uuid(),
title: faker.lorem.sentence()
})
},
resolvers: {
Mutation: {
updateItem: (_, { itemId, title }) => {
return {
message: 'Item updated'
}
}
}
}
})
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: new SchemaLink({ schema: schemaWithMocks })
})
return apolloClient
}
We are doing three things in this function.
First, we combine the two schemas and make it executable:
const executableSchema = makeExecutableSchema({
typeDefs: [schema, nextSchema]
})
Then we define the mock data to the schemas:
const schemaWithMocks = addMocksToSchema({
executableSchema,
mocks: {
Query: () => ({
items: [...new Array(5)]
}),
Item: () => ({
id: faker.datatype.uuid(),
title: faker.lorem.sentence()
})
},
resolvers: {
Mutation: {
updateItem: (_, { itemId, title }) => {
return {
message: 'Item updated'
}
}
}
}
})
Note how we add the query and type mocks in mocks
and mutation mocks in resolvers
.
We specify what each field in the type should return when it’s returned from a query, like this:
Item: () => ({
id: faker.datatype.uuid(),
title: faker.lorem.sentence()
})
We are using faker here to generate random values, but you can use any values you want.
If a query returns an array of a specific object type, then we specify how many instances of that object it should return by defining an empty array with a length:
Query: () => ({
items: [...new Array(5)]
})
The last step is to create the Apollo client and give it the schema with mocks through SchemaLink
(instead of uri
).
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: new SchemaLink({ schema: schemaWithMocks })
})
return apolloClient
How the workflow would look like
The cool thing about this mocking approach is that you don’t need to change the production code to use it.
However, every time the live schema is updated on the backend, you have to download it and define mocks for it if you want to use the mocked client.
So, the steps are:
- Download latest schema using the tools mentioned above. Or if the schema is not available on the backend yet, add the expected schema changes to
nextSchema.js
. - Add query and mutation mocks through
mock
andresolvers
inaddMocksToSchema
.