Let’s Learn GraphQL + Apollo Server and Client

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.


apollo_arch

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 function makeExecutableSchema.
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.

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

We want to work with you. Check out our "What We Do" section!