Photo by Daniele D’Andreti on Unsplash
Photo by Daniele D’Andreti on Unsplash

GraphQL: The Cure for REST APIs?

Frank Stepanski
17 min readAug 1, 2021

--

You are the disease and I’m the cure.

This article assumes a basic knowledge of REST APIs with Node/Express.

Web application programming interfaces (APIs) are an essential pillar of our connected world. Software applications use these interfaces to communicate from applications to smartphones to deeply hidden backend servers, APIs are absolutely everywhere.

What Makes an API Great?

A great API should make it easy to do the right thing and make it really hard to do the wrong thing.

Any developer should be able to come to your API and understand with relative ease what they can achieve and how to do it. Our API should guide developers towards the best way to use the API and also push them away from bad practices, all through the way we design it.

No matter what tool you use to build an API (REST, GraphQL, etc.), good API design should still be the first thought.

REST API Principles

Every HTTP request and response is made of two parts: the headers and the payload. The payload can be simple HTML displayed to the user or JSON when we are working with APIs.

A REST API is a collection of endpoints where each endpoint represents a resource. So, when a client needs data about multiple resources, it has to perform multiple network requests to that REST API and then put together the data by combining the multiple responses it receives.

At its core, an API is a data contract as an agreement on the general content of the request and/or response data. This contract is defined in the API documentation.

Check out Medium’s API documentation for a very thorough example.

The most common technique for handling API changes is to simply release a new, updated API in place of the old one. This is called versioning your API.

Some use cases for when versioning is necessary would be

  • Need to change the format of the response data.
  • Add or remove new endpoints.

There are different versioning methods available such as route versioning, query string versioning, and accept header versioning.

If you’re not careful, you could release a new API that’s incompatible with existing API consumers. When this happens you are “breaking” current API clients — and that’s a bad thing.

These trouble spots can make it difficult to manage a REST API as it scales.

Principles of GraphQL

GraphQL is two parts.

  • a specification for an API query language created by Facebook
  • a server engine capable of executing such queries.

Before GraphQL went open source in 2015, Facebook used it internally for their mobile applications in 2012. It was built as an alternative to the common REST architecture which was encountering performance issues (> 1 billion users) with the ever-changing and demanding mobile consumers.

For a history and technical break-down of GraphQL, grab some popcorn 🍿 and watch this presentation given by Lee Bryan at react-Europe 2015:

To summarize:

GraphQL is not specific to any backend or frontend framework, stack, or database. It can be used in any frontend environment, on any backend platform, and with any database.

  1. The client sends a query request to the same single API endpoint.
  2. The GraphQL runtime layer accepts the text request and communicates with other services in the backend stack.
  3. That runtime provides a structure called a schema for servers to describe the data to be exposed in their APIs to put together a suitable data response.
  4. It sends that data back to the client in a format like JSON.

Another popcorn flick: 🎥

A GraphQL operation on the front end is either a query (read), mutation (write), or subscription (continuous read). Each of those operations is only a string that needs to be constructed according to the GraphQL query language specification.

In a frontend web or mobile application, you can use GraphQL by making direct calls to a GraphQL server or with a client like Apollo or Relay (which will make the AJAX request on your behalf).

You can use a library like React (or React Native) to manage how your views use the data coming from a GraphQL service, but you can also do that with APIs native to their UI environments (e.g. DOM API or native iOS components).

GraphQL Advantages

Declarative Data Fetching

The client selects data along with its entities with fields across relationships in one query request. To retrieve all data in one request, a GraphQL query selects only the part of the data for the UI.

It offers a great separation of concerns: a client knows about the data requirements; the server knows about the data structure and how to resolve the data from a data source.

No Overfetching or Underfetching

What this means is the client has full control over which data it wants from the server. With RESTful APIs when you request a resource, you always get all that data from that resource which can lead to over-fetching.

If you want to fetch two different resources you need to make two separate calls to the server in a REST API. GraphQL lets you get many resources in a single request. This can solve under-fetching.

Strongly Typed

GraphQL is a strongly typed query language because it is written in the expressive GraphQL Schema Definition Language (SDL). Being strongly typed makes GraphQL less error-prone, can be validated during compile-time, and can be used for supportive IDE/editor integrations such as auto-completion and validation.

More flexibility to build developer tools

Having a type system makes it easier for building open-source developer tools such as Quell, Obsidian, Swell, GraphQL-Blueprint, AtomiQL, TorchQL, lexiQL, Peach QE, etc.

Managing REST Endpoints and Versioning

A common complaint about REST APIs is the lack of flexibility.

As the needs of the client change, you usually have to create new endpoints, and those endpoints can begin to multiply quickly. Development speed can be slow because setting up new endpoints and versioning (e.g. api/v1/, api/v2/), often means that frontend and backend teams have more planning and communication to do with each other.

With GraphQL, the typical architecture involves a single endpoint. The single endpoint can act as a gateway and orchestrate several data sources, but the one endpoint still makes the organization of data easier.

GraphQL Disadvantages

GraphQL Query Complexity
GraphQL doesn’t take away performance bottlenecks when you have to access multiple fields (authors, articles, comments) in one query. Whether the request was made in a RESTful architecture or GraphQL, the varied resources and fields still have to be retrieved from a data source.

Problems can arise when a client requests too many nested fields at once.

Frontend developers are not always aware of the work a server-side application has to perform to retrieve data, so there must be a mechanism like maximum query depths, query complexity weighting, avoiding recursion, or persistent queries for stopping inefficient requests from the other side.

GraphQL Rate Limiting

Whereas in REST it is simple to say “We allow only so many resource requests in one day”, it becomes difficult to make such a statement for individual GraphQL operations because it can be everything between a cheap or expensive operation.

That’s where companies with public GraphQL APIs such as Github come up with their specific rate-limiting calculations.

GraphQL Caching

Implementing a simplified cache with GraphQL is more complex than implementing it in REST. In REST, resources are accessed with URLs, so you can cache on a resource level because you have the resource URL as an identifier.

The reason is that GraphQL operates via POST by executing all queries against a single endpoint and passing parameters through the body of the request. That single endpoint’s URL will produce different responses, which means it cannot be cached — at least not using the URL as the identifier.

Ways of caching that can be done:

  • On the client with Apollo Client or similar libraries, cache the returned objects independently of each other, identifying them by their unique global ID.
  • Using HTTP caching but using GET instead of POST for query requests. HTTP cache mainly works on GET because it only reads from the server it does not write to the server. Other HTTP methods (POST, PUT, DELETE) are declined to be cached, because doing so may lead to loss of data and wrong info being displayed.

Let’s Start Using GraphQL

Server

At its core, building a GraphQL server mainly requires three main things:

  • A type system definition is written in the Schema Definition Language (SDL).
  • A runtime execution engine to fulfill the requested queries according to the type system.
  • An HTTP server ready to accept query strings.

The order of operations on the server:

  1. The core GraphQL engine accepts the schema definition upon instantiation, builds the type schema, and allows you to execute queries against that schema.
  2. The HTTP server accepts the GraphQL queries and then passes them to the core GraphQL engine. When the engine responds, the HTTP server then passes the JSON response back to the client.

In GraphQL the concept used to fulfill data for a certain field is called a resolver. So the query that is received is traversed field by field and executes resolvers for each field.

At their core resolvers are really just simple functions:

function resolveName(parent, arguments, context) { 
return parent.name;
}

A resolver is in charge of resolving the data for a single field. The GraphQL engine will call the resolver for a particular field once it gets to this point.

The basic execution of a GraphQL query often resembles a simple depth-first search of a tree-like data structure:

At every step or node of a GraphQL query, a GraphQL server will typically execute the associated resolver function for that single field.

A resolve function usually takes 3 to 4 arguments. The first argument is usually the parent object. This is the object that was returned by the parent resolver function. If you take a look a the image above, this means that the name resolver would receive the user object the user resolver returned.

The second argument is the arguments for the fields. For example, if the field user was called with an id argument, the resolver function for the user field would receive the value provided for that id argument.

Finally, the third argument is often a context argument. This is often an object containing global and contextual data for the particular query. This is often used to include data that could potentially be accessed by any resolver.

To put this into context, let’s say we had this query:

query { 
user(id: "abc") {
name
}
}

The GraphQL server would first call the user resolver, with our “root” object, the id argument, and the global context object. It would then take the result of the user resolver and use it to call the name resolver, which would receive a user object as a first argument, no extra arguments, and the global context object.

With a type system in place, and resolvers ready to execute for every field, we’ve got everything needed for a GraphQL implementation to execute queries.

But wait just one more thing… ✋

Schema First vs Code First

The main decision on how to implement GraphQL is whether to use a “schema-first” or “code-first” approach. These approaches refer to the way the type system is built on the server side.

With schema-first, developers mainly build their schemas using the Schema Definition Language (SDL). Schema-first approaches usually let you define resolvers, by mapping them to fields and types defined using the SDL.

But there are always trade-offs.

The SDL itself provides no mechanism to describe the logic that should be executed when a field is requested. It makes sense, the GraphQL schema language is made to describe an interface, but it’s not a powerful programming language that we need to execute network calls, database requests, or any logic we usually need in an API server.

The fact that we need to separate the schema description and what happens at runtime can be a challenge with a schema-first approach. When a schema grows large enough, it can be a challenge to ensure the type definitions and their mapped resolvers are indeed valid. Any change in the schema must be reflected in the resolvers, and that is true for the opposite as well.

As schemas grow large and more and more team members contribute to the schema, it’s often useful to encapsulate schema definition behavior in reusable functions or classes rather than typing it out entirely, which opens the schema to inconsistencies.

With a code-first (often called resolver-first) approach the schema is defined and built programmatically. The design process begins with coding the resolvers and the SDL version of the GraphQL schema is a generated artifact (created with a script manually).

The schema-first approach is still considered a standard for right now, but that may change in the future … [insert suspense sound effect]

We will focus on a schema-first approach in this article.

express-graphql

is an express middleware that gets applies to our /graphql endpoint.

It requires an object to be passed to it which is the schema. You also need to install the express and graphql package.

const express = require('express');
const {graphqlHTTP} = require('express-graphql');
const { buildSchema } = require('graphql');
// GraphQL schema language (just a string):
let schema = buildSchema(`
type Query {
hello: String
}
`);

A schema string is passed to buildSchema which builds a GraphQL schema in memory. While having a schema is great, it needs the logic on what this hello field should return when a client requests it, which is where the resolver comes in by mapping the fields and types.

// Define a resolver object that can augment the schema at runtime 
const resolvers = {
Query: {
hello: () => {
return 'Hello Frank!';
}
}
};
let app = express();app.use(
'/graphql',
graphqlHTTP({
schema: schema,
rootValue: resolvers,
graphiql: true,
})
);

You can see the resolver hello function maps to the Query field hello that describes what gets returned to the client.

Since we defined graphiql: true in building the GraphQL runtime we can view it in our browser with the GraphQL explorer (i stands for interactive). GraphiQL provides an IDE experience with autocompletion, documentation and a simple query editor.

Let’s build out the schema with some kung-fu movie data. ❤

const { buildSchema } = require('graphql');const movies = [
{
title: '5 Deadly Venoms',
releaseDate: '9/12/78',
rating: 4,
},
{
title: '36 Chambers of Shaolin',
releaseDate: '5/1/79',
rating: 4,
},
{
title: 'The Chinese Connection',
releaseDate: '9/9/72',
rating: 5,
},
{
title: 'Druken Master',
releaseDate: '12/14/78',
rating: 4,
},
];
let schema = buildSchema(`
type Movie {
title: String
releaseDate: String
rating: Int
}
type Query {
movie: [Movie]
}
`);
const resolvers = {
Query: {
movie: () => {
return movies;
}
}
};
module.exports = {
schema,
resolvers
}

A schema needs to have resolvers for all fields and are responsible for returning a result for that field.

resolvers object has a map of resolvers for each GraphQL Object Type:

  • the movie resolver maps to the Query movie type which returns the movies array

apollo-server

Apollo is an open source implementation of GraphQL.

apollo-server is an open-source GraphQL server implementing graphql with a built-in Express server. So you only need to install this one package.

gql is a template literal tag that parses GraphQL queries into an abstract syntax tree (AST).

So here is what happens:

  1. Parses the schema string to an AST.
  2. Transforms the AST into a GraphQLSchema.
  3. Adds the resolver functions to the GraphQLSchema object.

So an AST is a representation of the schema document and the GraphQLSchema object is a data structure that can resolve GraphQL queries.

const { gql } = require('apollo-server');const movies = [
...
];
// GraphQL schema
// note: need to call your schema,
typeDefs
const typeDefs = gql`
type Movie {
title: String
releaseDate: String
rating: Int
}
type Query {
movie: [Movie]
}
`;
const resolvers = {
Query: {
movie: () => {
return movies;
}
}
};
module.exports = {
typeDefs,
resolvers
}

We create an ApolloServer and pass in the schema and resolvers object.

const {typeDefs, resolvers} = require('./schema.js')
const { ApolloServer } = require('apollo-server');
const server = new ApolloServer({
typeDefs,
resolvers
});
server.listen(3000)
.then(() => {
console.log(`🚀 Server ready at http://localhost:3000`)
});

Apollo Server uses Apollo Sandbox (an instance of Apollo Explorer) as its GraphQL IDE.

bing, bang. boom. 💥

Apollo Sandbox has some nifty schema auto documentation features too.

Be aware that just because we are using GraphQL, that doesn’t mean you can not use good ole’ Postman. But remember, GraphQL queries need a POST request.

There are other GraphQL Server tools beyond Express GraphQL and Apollo Server such as graphql-yoga, graphql-helix, but the first two are the most popular as of right now.

GraphQL Language

Let’s review some of the GraphQL language terminology.

Types — is an object, like a customer or order or product.

type Product {
name: String!
description: String!
price: Float!
...
}

Product is the type. Types are the building blocks of GraphQL.

The whole point of GraphQL, is to allow frontend developers to traverse a graph of connected objects. When you design your types, make sure that you have as many type-based references as possible.

For example, you’d want your customer type to refer to your orders type, and your order type to refer to your customer type:

type Customer {
orders: [Order!]!
}
type Order {
customer: Customer!
}

Note: ! means it‘s a required field.

Fields — an attribute like name, description, price, and so on. Fields belong to types.

type Product {
name: String!
description: String!
price: Float!
parentCategory: Category!
reviews: [Review]
...
}

Following the name of the field, you’ll find its data type. Data types can be scalars (primitives) of the following type:

  • Strings (String)
  • Integers (Int)
  • Floats (Float)
  • Booleans (Boolean)
  • Unique identifiers (ID)

Fields can also have a data type of another type within the same GraphQL schema. In the previous example, the parentCategory field points to a Category type. When you build your query, you can retrieve field values from the referenced types:

query {
product(id: "94695736") {
category {
name
}
}
}

This would return something like the following:

{
"data" {
"product": {
"category": {
"name": "Men's Belts"
}
}
}
}

Rather than having a singular value, GraphQL also supports fields having multiple values through the use of brackets ([]). A field defining a single String would be represented as follows:

type Product {
reviews: [Review]
}

Arguments — You can pass arguments to any GraphQL operation, and they are often used for retrieving specific nodes in the graph.

query {
product(id: "94695736") {
name
}
}

GraphQL requires both the parameter name and value, which in this case is id: “94695736”.

Variables — Are prefixed with a $ and are immediately proceeded by the data type and then an exclamation point if it’s required. Queries can have an unlimited number of variables.

query orderHistory ($id: ID! $year: Int){}

This query requires the customer’s ID as the id variable and then optionally accepts the year as an input.

This ain’t a book so check out the official GraphQL docs to learn more. 📖

Client

A GraphQL client is software that runs in each client (web browser, native application, etc.) that handles the life cycle of connecting to the GraphQL server, executing queries, and receiving responses.

Any code that allows you to make an HTTP request is a client, from cURL on the command line to any of the thousands of JavaScript libraries that are available. Every programming language, framework, operating system, and so on has the means of making HTTP requests because HTTP-based API calls underpin modern IT.

Formal GraphQL clients offer basic HTTP request handling plus:

  • Low-level networking — retry policies, limit response sizes and timeouts.
  • Batching — group together multiple GraphQL queries.
  • Authentication — all clients require authentication with the GraphQL server before being able to execute queries.
  • Caching — can specify various cache directives for the type and field
  • Language-specific bindings — same functionality no matter what the programming language
  • Frontend framework integration — some frameworks are built entirely around GraphQL such as Facebook’s Relay.

All of the above can add up to a real-world advantage in programming time compared to just forming manual HTTP requests every time.

Apart from making native JavaScript AJAX calls to a GraphQL server, the most popular GraphQL clients as of right now are graphql-request, Apollo Client, and Relay.

graphql-request

is a super lightweight GraphQL client that works with any JavaScript web framework but does not have any caching or fancy features.

const {gql, GraphQLClient} = require('graphql-request');
const query = gql`
{
movie {
title
releaseDate
rating
}
}`;
const client = new GraphQLClient('http://localhost:3000/', { headers: {} })
client.request(query).then((data) => console.log(data));

Note: graphql-request still needs you to install the graphql package.

Since we are in Node.js we see the results in the terminal:

Apollo Client 🥇

As of right now, this is the de-facto standard GraphQL library — works w/ React, Angular, Vue, etc.

The Apollo Client manages the complexity of orchestrating all queries. It is responsible for scheduling, optimizing, caching, managing state, and sending queries to a GraphQL-endpoint.

Be aware that Apollo Client is optimized for React as its view layer, so if you are not using React or any frontend framework, I personally suggest using the lightweight graphql-request.

We implement an ApolloClient client instance to do the GraphQL data fetching anywhere in our React app component tree. This is made possible by having the root App component a parent of the ApolloProvider component.

The ApolloProvider component leverages the React’s Context API to provide the client with any component that needs it.

import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloClient, InMemoryCache, ApolloProvider } from
"@apollo/client"
import App from './App';
const client = new ApolloClient({
cache: new InMemoryCache(),
uri: 'http://localhost:3000/',
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);

In the App component, we define the GraphQL query string with the gql template tag that parses into an AST. Then we render the data sent back from useQuery which also gives us two properties for handling loading and errors.

import { useQuery, gql } from "@apollo/client"const MOVIES =  gql`
{
movie {
title
releaseDate
rating
}
}
`;
function App() {

const { loading, error, data } = useQuery(MOVIES);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
const movies = data.movie.map(
({ title, releaseDate, rating }, index) => (
<li key={index} style = {{ lineHeight: "1.5"}}>
{title}, {releaseDate}, rating: {rating}
</li>
));
return (
<>
<ul style={{ listStyle : "none", paddingLeft: 5 }}>
{movies}
</ul>
</>
);
}
export default App;

And just like Apollo Server, we can use Apollo’s GraphQL IDE for testing.

And just like that, we created a GraphQL React client and a GraphQL API. 😊

--

--

Frank Stepanski

Engineer, instructor, mentor, amateur photographer, curious traveler, timid runner, and occasional race car driver.