GraphQL
Basics
Why GraphQL and how it works
GraphQL is better for larger projects where you have a ton of endpoints and instead you just want to fetch the exact data you need.
Here are the core benefits of GraphQL:
- Avoid over-fetching: You avoid fetching more data than you need because you can specify the exact fields you need.
- Prevent multiple API calls: In case you need more data, you can also avoid making multiple calls to your API. In the case above, you don't need to make 2 API calls to fetch
userandaddressseparately. - Less communication overhead with API developers: Sometimes to fetch the exact data you need, especially if you need to fetch more data and want to avoid multiple API calls, you will need to ask your API developers to build a new API. With GraphQL, your work is independent of the API team! This allows you to work faster on your app.
- Self-documenting: Every GraphQL API conforms to a "schema" which is the graph data model and what kinds of queries a client can make. This allows the community to build lots of cool tools to explore & visualise your API or create IDE plugins that autocomplete your GraphQL queries and even do "codegen". We'll understand this in more detail later!
GraphQL app flow

This is how it works:
-
Define the schema and resolvers on your server, then host the graphql endpoint at a
POST /graphqlroute.- schema: the type definitions defining interfaces made available to run queries and mutations on.
- query: semantics for defining a graphQL function to fetch defined interfaces and resources.
- mutations: semantics for defining a graphQL function to mutate defined interfaces and resources, but works the exact same as query, it's just semantics.
- resolvers: the actual code business logic you write that defines what data to return for when queries and mutations are invoked.
- schema: the type definitions defining interfaces made available to run queries and mutations on.
-
Invoke a query or mutation: From the frontend, invoke a query or mutation using graphQL syntax to the
POST /graphqlroute on a server.
// Setup a GraphQL client to use the endpoint
const client = new Client("http://localhost:4000/graphql");
// Now, send your query as a string (Note that ` is used to create a multi-line
// string in javascript).
client.query(`
query {
user {
id
name
}
}`);
- Receive data: GraphQL always returns data as JSON with a status code of 200, and the data being returned under the
datakey.
Here's the basic client-server flow:
- Note that the GraphQL query is not really JSON; it looks like the shape of the JSON you want. So when we make a 'POST' request to send our GraphQL query to the server, it is sent as a "string" by the client.
- The server gets the JSON object and extracts the query string. As per the GraphQL syntax and the graph data model (GraphQL schema), the server processes and validates the GraphQL query.
- Just like a typical API server, the GraphQL API server then makes calls to a database or other services to fetch the data that the client requested.
- The server then takes the data and returns it to the client in a JSON object
GraphQL vs REST
GraphQL fetches data in terms of graphs while REST is just based on resources.
| Requirement | REST | GraphQL |
|---|---|---|
| Fetching data objects | GET | query |
| Inserting data | POST | mutation |
| Updating/deleting data | PUT/PATCH/DELETE | mutation |
| Watching/subscribing to data | - | subscription |
- Type systems: GraphQL is strongly typed while REST is not.
- REST API: In REST APIs, there isn't a concept of a schema or type system.
- GraphQL: On the other hand, GraphQL has a strong type system to define what the API looks like using a schema.
- Caching: GraphQL does not have automatic caching support because all of its requests are POST requests, while REST GET requests can be cached easily, but client-side libraries like tanstack query and apollo make caching easier.
GraphQL caching
With REST APIs, all the GET endpoints can be cached at the server side or using a CDN. They can be cached by the browser as well and bookmarked by the client for frequent invocations. GraphQL doesn't follow the HTTP spec and is served over a single endpoint, usually (/graphql). Hence the queries cannot be cached in the same way as REST APIs.
However caching on the client side is better than REST because of the tooling. Some of the clients implementing caching layer (Apollo Client, URQL) make use of GraphQL's schema and type system using Introspection to allow them to maintain a cache on the client side.
Core concepts
Here are the structural terminology terms for what the server does to create a GraphQL schema that is then able to be served at an endpoint and successfully fetched from:
- schema: A schema is defined with fields mapped to types and serves as a contract between the client and the server.
- fields: the individual interfaces in graphQL documents, which represent single function invocations or resources.
- resolvers: the actual business logic that defines how code should be executed in order to fulfill queries, mutations, and subscription requests to schemas, dealing with the underlying data like a database.
Here are the terminology terms concerned with the client-server response cycle for GraphQL:
- graphQL operation: the string representation of a query, mutation, or a subscription that a client invokes to fetch or mutate schema data from the server.
- document: The content of a GraphQL request string is called the GraphQL document.
- Documents contain one or more graphQL operations, and this is what the client sends to the server to invoke all those operations.
Basically here are the steps of a client-server response cycle in graphQL:
- Client creates document: Client defines many graphQL operations, which include queries, mutations, and subscriptions, all in a document.
- Client sends document to server: Client sends a
POST /graphqlrequest sending the document along as a string in the request body. - Server resolves document: resolvers and schemas automatically handle what the document wants to fetch and mutate, and then sends back data.
- Client receives data: Client receives data resolved from server as JSON
All about operations
Operations: Basics
When a client is making a graphQL call to the server, we call that a graphQL operation, of which there are three types:
- query (a read-only fetch)
- mutation (a write followed by fetch)
- subscription (a long‐lived request that fetches data in response to source events.)
All of these graphQL operations takes in graphQL documents to request.
NOTE
All of these are technically the same, but semantically are meant to be used differently, and that is evident in how client-side libraries follow conventions when performing these different type of operations to the server even though business logic is the one that decides the actual difference between these operations.
Operations: Aliases
When you are fetching information about an author, let's say you have two images, different sizes and you have a field with an argument to do that.
In this case, you cannot use the same field twice under the same selection set and hence an Alias would be helpful to distinguish the two fields.
query fetchAuthor {
author(id: 1) {
name
profile_pic_large: profile_pic(size: "large")
profile_pic_small: profile_pic(size: "small")
}
}
Operations: Fragments
Fragments make GraphQL even more reusable. If there are some parts of your document that reuses the same set of fields on a given type, then fragment can be powerful.
fragment authorFields on author {
id
name
profile_pic
created_at
}
query fetchAuthor {
author(id: 1) {
...authorFields
}
}
query fetchAuthors {
author(limit: 5) {
...authorFields
}
}
Operations: Directives
Directives are identifiers which add additional functionality without affecting the value of the response but can affect what response comes back to the client.
The identifier @ is optionally followed by a list of named arguments.
Some default server directives supported by GraphQL spec are:
@deprecated(reason: String)- marks the field as deprecated@skip (if: Boolean)- Skips GraphQL execution for this field@include (if: Boolean)- Calls resolver for an annotated field, if true.
Here is an example using directives.
query ($showFullname: Boolean!) {
author {
id
name
fullname @include(if: $showFullname)
}
}
Queries
There are two types of queries:
- anonymous query: a query without a name
query {
todos {
title
}
}
- named query: a query that you provide a name for, which is the best practice because it helps when debugging.
query getTodos {
todos {
title
}
}
queries with arguments
In most API calls, you usually use parameters. e.g. to specify what data you're fetching.
- If you're familiar with making
GETcalls, you would have used a query parameter. - For example, to fetch only 10 todos you might have made this API call:
GET /api/todos?limit=10.
The GraphQL query analog of this is arguments, which are key-value pairs that you can attach to a "field" or "nested object".
GraphQL servers come with a default list of arguments, but you can also define custom arguments.
For both queries and fields, you can use the default list of arguments and "invoke" fields and queries with those built-in arguments, which contain these:
limit: the number of records to returnoffset: the number of records to skipwhere: conditional filtering of records based on the values of certain properties of the field.order_by: how to sort the list of records that are returned, with values following this syntax:
field(order_by: { property_name: desc/asc })
Here is an example of how to pass arguments to a field in a query.
query {
author(limit: 5, offset: 10) {
id
name
}
}
variables
Until now, you hardcoded the arguments in the queries. In real-life applications, though, the arguments might come from different parts of your application, such as filters for example. So you will pass them dynamically to your queries.
In GraphQL, you can pass arguments dynamically with the help of variables:
query ($limit: Int) {
author(limit: $limit) {
id
name
}
}
The variable(s) is defined at the top of the operation and the value for the variable can be sent by the client in a format that the server understands.
Typically variables are represented in JSON like below:
{
limit: 5
}
multiple query operations
A single document can have multiple operations it in at once.
Here is an example of a document that has multiple query operations
query fetchAuthor {
author(id: 1) {
name
profile_pic
}
}
query fetchAuthors {
author(limit: 5, order_by: { name: asc }) {
id
name
profile_pic
}
}
limit and offset
{
todos(limit: 5, offset: 5) {
title
is_completed
is_public
}
}
order_by
The order_by key lets you sort the list of records that are returned, with values following this syntax:
field(order_by: { property_name: desc/asc })
Here is an example where we sort the todos field returned on the record in descending order based on the created_at field.
query {
users (limit: 1) {
id
name
todos(order_by: {created_at: desc}, limit: 5) {
id
title
}
}
}
where
The where argument lets you conditionally filter records based on the property value of fields:
{
todos(where: {is_public: {_eq: false}}) {
title
is_public
is_completed
}
}
You can also use the where argument multiple times in one query. Let's say you want to see all the public notes from a specific user:
{
users(where: {id: {_eq: "61dd5e7dc4b05c0069a39att"}}) {
name
todos(where: {is_public: {_eq: true}}) {
title
is_public
}
}
}
Subscriptions
GraphQL Subscriptions are implemented using the WebSocket protocol, enabling us to create a persistent connection between the server and client. The connection stays open until either party terminates it.
There are two ways to implement a subscription in graphQL:
subscriptionoperation: use thesubscriptionkeyword to make a query a subscription type, which triggers automatic use of websockets.
subscription {
todos {
id
created_at
is_completed
is_public
title
}
}
- live queries: Use the
@livedirective to decorate a normal query and make it a subscription without the automatic websockets, but now you have to write the business logic yourself to implement WebSockets or some other real-time data solution to actually handle the live query.
query @live {
todos {
id
created_at
is_completed
is_public
title
}
}
Live queries vs subscriptions
A Live Query watches the query result and whenever it changes, the server returns the new results to the client by invoking the resolver.
A subscription uses websockets behind the scenes to keep the connection open and facilitate real-time data flow from a data store to a client.
Here are the main differences:
- graphQL support: One significant difference is that Subscriptions are defined in the GraphQL Specification, whereas Live Queries are not. That means there is no official definition of a Live Query.
- realtime: Another difference is that Subscriptions respond to events, sending back data on insertions, while live queries are reactive and return new results if the arguments passed to a query changes.
- how subscriptions work: For example, you might have a Subscription that reacts to an insertion. When the insertion occurs, the server sends back the new data to the client.
- how live queries work: On the other hand, Live Queries watch the latest result of a query and whenever it changes, the server returns the latest results to the client. Rather than responding to an event, they monitor for changes in the query result.
Resolvers
The basic signature of a resolver looks like the following:
resolverFunc(data, args, context, info)
data- previously fetched data from the parentargs- key-value pairs of arguments, optionalcontext- state information per request, typically used for auth logicinfo- metadata about the selection context for traversal