Hello boys and girls! In this article, we’re going to talk about GraphQL using the Apollo Platform for both server-side and client-side.
First of all, we need to know what GraphQL is:
GraphQL is a typed query language and data runtime that consults data on APIs developed by Facebook. It is strongly typed to enable excellent tooling for development.
GraphQL has some important differences betwen other strategies, like REST. It uses the same route for all resources, while in REST, for instance, you have multiple routes for resource URLs. Furthermore, in REST, the shape and size of the data are determined by the server, while in GraphQL the client has the power to request any data they want.
The query in GraphQL is dynamic, instead of having its result shape hardcoded as in REST API. This alllows something called No “overfetching”, that is when there’s no extra data (that won’t be used) in the response. While GraphQL allows the client to retrieve data fit to a specific use case, REST APIs return predefined data. GraphQL is called “Graph” because it traverses the query graph sent by the client and proceeds to the resolvers to go ahead and fetch all of the relational data requested by the client.
GraphQL can be divided into four main concepts: Schema Definition Language (SDL), Queries, Mutations, and Subscriptions.
Schema Definition Language (SDL)
The Schema Definition Language consists of an object representation. When we work with GraphQL to make queries, it needs to know what properties/attributes are available. For this reason, we need to define one or more schemas to provide a sort of a map of the data we intend to obtain.
One of the main ideas behind GraphQL is to work alongside any language or framework. Following this premise, they created their own language for schema declaration, SDL.
Look at the SDL Schema example below:
type Team {
id: ID!
name: String!
foundation: Int!
logoUrl: String!
players: [Player]
}
Queries
Queries are responsible for fetching data from the API (reads).
Example:
A GraphQL query operation with the query name findAllTeams
, which returns an array of all teams and their players
(an array too). The query name is a custom entry point defined on Apollo Server by the programmer (see more details down below):
query {
findAllTeams {
id
name
foundation
logoUrl
players {
name
age
photoUrl
position
}
}
}
A GraphQL query operation with only the query result for teams
. In this case, teams
is derived solely from the schema:
query {
teams {
id
name
foundation
logoUrl
players {
name
age
photoUrl
position
}
}
}
A GraphQL query operation without the query
keyword and with only the return result: teams
teams {
id
name
foundation
logoUrl
players {
name
age
photoUrl
position
}
}
The three queries are functionally equivalent. In GraphQL, the query and name keywords are optional. However, it’s recommended to use both of them to distinguish the operation type and make it easier for your teammates to know what your query is doing.
When we ask for data, we get a response back in a similar shape as the query, most commonly in JSON format. That is called the GraphQL query result.
{
"id": "123456abc",
"name": "SE Palmeiras"
"foundation": "1914",
"logoUrl": "chest.png",
"players": [
"name": "Ronny",
"age": 25,
"photoUrl": "profile.png",
"position": "forward",
]
}
}
Mutations
Mutations are responsible for making actual changes to server-side data, running operations like create
, update
and delete
. When the back-end receives the mutation commands, it uses them to perform the desired operation.
Mutations follow the same structure as queries, but they require using the mutation
keyword, the input parameters, and a returning value as the result of the operation to the client.
Example:
mutation { // Mutation keyword
storeTeam (
name: "Palmeiras 3", // Input parameters
foundation: 1914,
logoUrl: "url"
) {
name // Return value
}
}
Subscriptions
Nowadays, real-time features are in demand for increased interactivity on web applications. A good example is YouTube; when a new video from some channel subscription is posted, reloading the page to see the notification count bump is not needed at all.
So, subscriptions on GraphQL provide a way to subscribe to some back-end operation (like a mutation) and receive data from it. This corresponding operation uses the publish & subscribe (pubsub) message pattern, which is usually implemented with the WebSocket Protocol.
Apollo, Server & Client
Apollo Server
Apollo Server aims to provide an open-source GraphQL server compatible with any client that uses GraphQL for data queries, including Apollo Client, which will be covered later in this post.
The figure below illustrates how Apollo Server acts, providing an interface that sits between REST APIs, microservices, and databases — and web, mobile, or even desktop client applications.
You can use Apollo Server as:
- A standalone GraphQL server (also in a serverless environment);
- An add-on to your application’s existing Node.js middleware (such as Express or Fastify);
- A gateway for a federated data graph — consists of consuming data from several services and providing it to the client through a gateway.
Apollo Client
Apollo Client is a library that provides state management for JavaScript, allowing you to manage local and remote data with GraphQL. It can use it to fetch, store and modify application data while automatically updating the end-user interface.
Apollo Client helps you structure your code in a cost-effective, predictable, and declarative way through modern development practices. The core @apollo/client
library provides integration with React (which is the focus of this article).
The Apollo community maintains integrations for other libs and frameworks, such as Vue.js and Angular.
Main features
- Declarative data fetching: Write a query and receive data without manually tracking loading states;
- Excellent developer experience: Enjoy helpful tooling for TypeScript, Chrome / Firefox devtools, and VS Code;
- Designed for modern React: Take advantage of the latest React features, such as hooks;
- Incrementally adoptable: Drop Apollo into any JavaScript app and incorporate it feature by feature;
- Universally compatible: Use any build setup and any GraphQL API;
- Community-driven: Share knowledge with thousands of developers in the GraphQL community.
Use case application
For this article, we developed an application that deals with information about soccer clubs and athletes to show in practice how Apollo Server and Client work.
The application consists of a back-end server that provides an API in GraphQL that supplies data for CRUD using the Apollo Server. The data is stored in a NoSQL database (via MongoDB), and there’s a SPA based on React + Apollo Client for consuming the data.
And the two GraphQL types defined are:
type Team {
id: ID!
name: String!
foundation: Int!
logoUrl: String!
players: [Player]
}
type Player {
id: ID!
name: String!
age: Int!
photoUrl: String!
position: String!
team: Team!
}
The back-end
The back-end application has the following structure:
src/
|__/database/
|__/models/
|__/resolvers/
|__/schema/
|__/services/
app.ts
server.ts
The main dependencies used are:
apollo-server
: The core library for Apollo Server itself, which helps you define the shape of your data and how to fetch it;graphql
: The library used to build a GraphQL schema and execute queries against it.
Let’s focus on the two main folders: schema
and resolvers
. The schema
folder contains two important files:
schema.graphql
has all the types, queries, and mutations;index.ts
joins the schema definitions with resolvers through the functionmakeExecutableSchema
.
const schema: GraphQLSchema = makeExecutableSchema({
typeDefs, // from schema.graphql
resolvers,
});
export default schema;
The resolvers you will see below are responsible for using the schema definitions and providing them (queries and mutations) at the root of the Graph structure. The example below shows how resolvers are used:
const resolvers: IResolvers = {
...teamResolvers,
};
export default resolvers;
teamResolvers
is an object that contains all GraphQL operations currently implemented: mutations, queries, and subscriptions focused on the Team
type, creating players from the storeTeamPlayers
mutation operation.
export default {
Query: {
async findAllTeams(_: void, args: void) {
const teams: any[] = await index();
return teams;
},
async findTeamPlayers(_: void, { teamId }) {
const players: any[] = await indexTeamPlayers(teamId);
return players;
},
},
Mutation: {
async storeTeam(_: void, { name, foundation, logoUrl }) {
const newTeam = await store({ name, foundation, logoUrl });
pubsub.publish(TOPICS.TEAM_ADD, {
teamAdded: { id: newTeam.id, name, foundation, logoUrl },
});
return newTeam;
},
async storeTeamPlayers(_: void, { teamId, players }) {
const teamPlayers = storeTeamPlayers({ teamId, players });
return teamPlayers;
},
async removeTeam(_: void, { teamId }) {
const team = removeTeam(teamId);
return team;
},
},
Subscription: {
teamAdded: {
subscribe: () => pubsub.asyncIterator([TOPICS.TEAM_ADD]),
},
},
};
To me, the most interesting item to explain is Subscription
because working with real-time data is really cool. So, the workflow is simple: teamAdded
is a subscription that can be subscribed to (of course) by any client through a topic (TOPICS.TEAM_ADD
). After subscribed, the client can receive the contents of the new team that was added to the database in real-time. To do that, the graphql
lib relies on the WebSocket Protocol to establish real-time communication between client and server.
The front-end
Now, we will talk about the front-end SPA and its structure. So, the front-end uses the structure below:
src/
|__/components/
|__/domain/
|__/images/
|__/infra/
|__api/
|__/pages/
|__/router/
|__/services/
App.tsx
index.tsx
Let’s focus on the infra/api
and services
folders, which are at the core of the SPA. The Infra/api
folder contains the configuration necessary to establish the connection between the SPA and the back-end GraphQL API.
const httpLink = new HttpLink({
uri: process.env.REACT_APP_BACKEND_API,
});
const webSocketLink = new WebSocketLink({
uri: ${process.env.REACT_APP_BACKEND_WEB_SOCKET}
,
options: {
reconnect: true,
},
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
webSocketLink,
httpLink
);
export const api = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
Apollo Client also provides a caching feature to avoid unnecessary requests to the same data through the cache
property, which is an instance of InMemoryCache
. To be able to consume and change the data, the client needs to use ApolloProvider
on the parent component, which in our SPA is App.tsx
— which makes it possible to use GraphQL operations like mutations, queries, and subscriptions, implemented as services.
import { api as client } from "./infra/api";
function App() {
return (
<ApolloProvider client={client}>
<div className="app">
<header className="app-header">
<h1>World Teams</h1>
</header>
<Router />
<div className="app-footer">
<span>Made with love by Gabriel Quaresma</span>
</div>
</div>
</ApolloProvider>
);
}
export default App;
Implementing GraphQL operations with Apollo Client is simple. You just need to build the operations using the GraphQL Query Language (GQL). Take a look at code below:
import { gql } from "@apollo/client";
export const TEAMS = gql`
query {
findAllTeams {
id
name
foundation
logoUrl
players {
name
age
photoUrl
position
}
}
}
`;
export const STORE_TEAM = gql`
mutation StoreTeam($name: String!, $foundation: Int!, $logoUrl: String!) {
storeTeam(name: $name, foundation: $foundation, logoUrl: $logoUrl) {
id
}
}
`;
export const STORE_TEAM_PLAYERS = gql`
mutation StoreTeamPlayers($teamId: String!, $players: [PlayerInput!]!) {
storeTeamPlayers(teamId: $teamId, players: $players) {
name
team {
name
}
}
}
`;
export const REMOVE_TEAM = gql`
mutation RemoveTeam($teamId: String!) {
removeTeam(teamId: $teamId) {
id
}
}
`;
export const TEAM_ADDED_SUBSCRIPTION = gql`
subscription TeamAdded {
teamAdded {
id
name
foundation
logoUrl
}
}
`;
To use these operations on React Components, you need to use hooks like useQuery
, useMutation
and useSubscription
. Let’s show some examples:
const { loading, error, data, refetch } = useQuery(TEAMS);
const { data, loading } = useSubscription(TEAM_ADDED_SUBSCRIPTION);
const [storeTeam] = useMutation(STORE_TEAM);
useQuery
and useSubscription
return properties such as data
and loading
, which correspond to the resulting data and the status of the request (done or pending). It also provides an error
property that can be handled on the client-side, and for queries it provides a function called refetch
, which allows the client to make the same request again to retrieve new data from the backend API.
- Source of the use case application on Github: brown-bag-graphql
Advantages & Disadvantages
Altough GraphQL is really awesome, it is no silver bullet.
Bellow, we have listed some pros and cons of using GraphQL so you can better undestand it’s applications and limitations.
Pros:
- GraphQL Server provides a single POST endpoint that is used by all use cases of the application;
- The client chooses exactly the data that it wants;
- The client can fetch all the data it needs with a single request instead of many requests that are common with REST.
Cons:
- HTTP Status code: GraphQL queries always return 200 OK, regardless of whether the query was successful or not. If your query is not successful, your JSON response will have a top-level error key with the associated error messages and the stacktrace. In some cases, this makes error handling much more complex and may lead to additional complexity for operations such as monitoring;
- Query complexity: Sometimes, one query asks for too much data, and handling that on the client-side can be hard;
- Database overload: In some cases, one query can execute many calls to your database that may overload your server. So, balancing and devising efficient queries can save your infra;
- Cache support: You will need to setup your own caching strategy, which means relying on another library like Apollo.
Conclusions
- A GraphQL operation can either be a query or a mutation;
- We learned that the query and name keywords for a GraphQL operation are optional, but it’s recommended to use both to keep your queries readable and understandable;
- The best way to determine whether you want to write a query or a mutation is to ask yourself if you’re fetching data (query) or modifying the state in the server (mutation).
This article was wrote by Gabriel Quaresma and Rafael Paiva.
References
- GraphQL: The revolutionary query and data manipulation language for APIs by Akira Hanashiro (in Portuguese);
- Advantages and Disadvantages of GraphQL;
- Introduction to Apollo Server;
- Introduction to Apollo Client.
We want to work with you. Check out our "What We Do" section!