GraphQL from the Ground Up
Building APIs is a craft. Your API product must be stable, accurate, well documented, performant, and meet the informational demands of your clients — no small task.
In this series, we'll take a close look at GraphQL, a trending API technology that aims to help you deliver efficient and capable APIs. GraphQL's unique and nuanced view of the client-server relationship offers interesting benefits for API delivery.
We'll explore GraphQL from the ground up, but first, let's take a look at where we came from.
Some quick RESTful history….
Before we dive into the novel aspects of GraphQL, it's helpful to quickly remember some API history. Representational State Transfer, or REST, was first conceived by Roy Fielding in his dissertation at University of California in 2000. At the core of REST design is the concept of resources, uniquely identified by URLs. Clients interact with the resources using the URL, and express CRUD-like operations using HTTP verbs (GET, POST, PUT, DELETE, etc).
The widespread adoption of REST can be attributed to its simplicity and generally accepted standards. However, as with all technologies, understanding trade-offs is key to successful adoption, and GraphQL and REST are no exceptions. Let's look at how GraphQL re-envisions API design.
GraphQL core concepts
The GraphQL specification was first developed at Facebook in 2012 and later open-sourced in 2015. Today, the project is governed by the GraphQL Foundation, which is hosted under the Linux Foundation. With its clear, open specification and numerous language implementations, the GraphQL project has interesting potential benefits.
The key to GraphQL's unique approach to API development lies in its name. GraphQL services adhere to a type system that describes complex objects and the scalar data type members they contain. Every GraphQL service advertises a schema that defines its objects and the relationships between them, reminiscent of a graph of nodes and edges. Additionally, the service specifies a set of queries that clients are permitted to invoke over the graph.
Let's examine GraphQL in practice by looking at a service for a hypothetical airline. We'll start with the theoretical concepts and implement a functioning example in a future post. The following are snippets of GraphQL Schema Language that will define our service:
Our service specifies six types of entities: Routes, Flights, Bookings, Customers, Customer Information, and Payment Methods.
Entities may have either a one-to-one or one-to-many relationship. One-to-many definitions adhere to a typical array-style syntax. Take, for example, the field Customer.bookings
:
This syntax indicates that the Customer.bookings
field is an array of Booking
objects. The exclamation mark on type definitions indicates non-nullability, meaning that neither the array of bookings nor the Booking
objects contained in the array can be null.
A one-to-one relationship is shown in the Customer.information field
:
Simply put, each Customer
has an instance of CustomerInformation
which cannot be null.
All services must specify a Query
type which provides an entry point for clients to read from the graph.
In this example, clients have three queries available:
- The
routes
query returns a non-null array of non-nullRoute
objects. The query also accepts an argument,origin
. Note that there is no exclamation mark on this argument, and that indicates the argument is optional for the client. - Similarly, the
flights
query returns flight information, but here the date argument is required which is indicated by the exclamation mark on the argument. - The
bookings
query requires a customer ID field and returns a non-null array of non-null bookings.
Mutations serve as the write entry point for clients. GraphQL Schema Language allows the service to define mutations that accept complex parameters and specify what data is required.
Our service supports the createBooking
mutation, allowing a client to create a new Booking
and update their customer information in one call. For input
types, the exclamation mark indicates that the field is required. In our example, you can see that the booking
input is required while customerInformation
is optional.
While GraphQL services can neatly declare the objects and data they support using the GraphQL Query language, things start to get even more interesting from the client's perspective. Let's see how.
Read what you need
In a significant shift, GraphQL inverts control over the data model returned from queries. Clients specify precisely the data they need in every query, and the service returns only the fields that satisfy the request.
Given the service-provided schema, clients can craft specific queries to request only the subset of data they require. Here's an example query a client might invoke to return a list of flights for a specific date:
In this query example, we’re requesting Flight
instances for March 20, 2024. We're interested in flight numbers, routes, and scheduled departure times. For each route, we’re seeing the origin and destination airport codes. Note that clients can specify subsets of all available fields. We'll explore the benefits of this design later.
Write once
GraphQL employs a technique known as Mutations for data updates. A single Mutation can encompass multiple objects and fields, each representing different update operations. This approach allows for multiple related updates to be executed within a single request.
Consider the following example of a client mutation request to create a new Booking
, which also updates a customer's frequent flier number:
This request specifies not only the data to update, but the values the client wants returned from the mutation — in this case, only the ticket_number
. Depending on the API design of alternative solutions, this could reduce the number of round trips to the server, leading to more efficient resource utilization.
Real-time updates
GraphQL natively supports real-time updates provided by the service through Subscriptions. Subscriptions are defined in the service-provided schema, just like queries or mutations.
The service defines a subscription using the Subscription type. For instance, to define a stream of new Flight objects, the service might define the following:
The client can subscribe to this new flight stream by submitting a subscription
request and specifying the objects and fields it would like to receive:
While the GraphQL specification doesn't enforce a particular transport technology for subscriptions, many GraphQL libraries and services utilize WebSockets.
In the next installment of this GraphQL series, we'll build a working example of this hypothetical airline service. Before we get there, let's compare and contrast GraphQL and REST tradeoffs to help make an informed decision about our API technologies.
GraphQL vs REST
The choice between GraphQL vs REST largely hinges on your application’s specific requirements and the trade-offs you’re prepared to accept. Let's break the decision down into some key aspects.
Efficiency
In REST APIs, the data that clients receive from queries is typically fixed by the resource model. This arrangement can present two problems for clients: under-fetching and over-fetching of data. With under-fetching, a REST client may need to issue multiple queries to different resources to acquire all necessary data. Over-fetching, conversely, occurs when the client has to process and discard surplus information. GraphQL, by contrast, allows clients to specify only the data they need, thereby avoiding both over and under-fetching issues.
Usually, read efficiency is more crucial than write efficiency, as you often write data once but read it multiple times. With GraphQL, it might be possible to achieve efficiency in both read and write operations. GraphQL mutations allow a client to specify a graph of changes that can be applied across multiple objects. In contrast to REST, which typically designs resources around entities, the client might need to issue multiple write requests to different endpoints to achieve the same outcome.
The mutation example provided earlier illustrates this difference. We create a new flight and update the customer's frequent flier number in a single request. In a typical REST-based design, these resources likely need to be updated across multiple requests.
Versioning
Evolving APIs once they're in production is tricky business. In REST, the service dictates the form of the data the client will receive. If a service decides to add or remove a field, the client is at the mercy of the service and must write code that is resilient to these changes.
On the other hand, with GraphQL, the client only requests the fields it needs, making it less vulnerable to these changes. Services can add fields freely because clients are only receiving and processing the fields they specify. Removing fields may have less impact because some clients will not be receiving them at all. Additionally, GraphQL services may be able to audit requested fields and determine customer impact before removing them. Finally, GraphQL natively supports a @deprecated directive and specifications for phasing out required fields.
Real-time events
Certain applications require real-time, or streaming, updates from a service. An example from our fictional airline example could be in-progress flight status updates. REST doesn't provide a specific solution for real-time updates, as its specification focuses on sessionless and stateless operations. Websockets, a common protocol for bi-directional communication in APIs, often serve as a common solution for services wishing to provide streaming updates.
GraphQL, however, provides an opinionated solution with Subscriptions. With Subscriptions, clients can apply a filter from the service-provided schema, and receive a "Response Stream" composed of updated objects and data elements.
Many GraphQL libraries implement Subscriptions on top of the WebSocket protocol. While non-GraphQL APIs can offer similar functionality, they may need to implement more of the application layer versus what's required with GraphQL.
Maturity
All technology comparisons essentially boil down to trade-offs. In the REST versus GraphQL debate, REST claims significant advantages in simplicity and maturity. REST-based designs have been in production for decades and are supported by a comprehensive tooling ecosystem. Moreover, your team's developers are likely well-versed in REST-based designs and principles, which can be critical for maintaining development velocity and quality.
By comparison, GraphQL is an emerging technology and may be unfamiliar to some of your team members. The added benefits of GraphQL come with additional complexity and a learning curve for many developers.
Summary
For certain types of applications, GraphQL can offer significant advantages. Mobile applications, particularly those dealing with limited bandwidth and network connectivity, may greatly benefit from GraphQL's efficient network usage. Data-intensive applications, even within local networks, can leverage the optimized query capabilities to minimize network transmissions and client processing requirements. Greenfield applications seeking to implement real-time streaming might prefer the GraphQL opinionated solution over common low-level WebSocket solutions.
For systems already entrenched in RESTful design, a migration to GraphQL may be appropriate. GraphQL implementations will enable you to layer on GraphQL capabilities to existing REST-based services. Providing a query-only implementation of GraphQL, which resolves the queries to current REST-based APIs, may be a good stepping stone to full GraphQL solutions. This is precisely what we'll do in the next post of this series, where we'll build a functioning GraphQL service based on our fictional airline. See you there!