How to Communicate with an API: An Introduction to GraphQL

Sylwia Zacharek
Sylwia Zacharek
August 4, 2026 | Software development
GraphQL

Intro

How many times have you struggled with a REST endpoint that returned too little and too much information at the same time?

Example:
You wanted a book’s title and its authors’ names, you got a number of pages and a genre and needed a second request for authors’ names.

What if I tell you that there’s a language that solves this problem?

query GetBook { 
  book(id: "1") { 
    title 
    authors { 
      name 
    } 
  } 
} 

With a simple query you get exactly what you want. Welcome to the world of GraphQL!
Let’s dive into how it works.

What is GraphQL?

In simple words, GraphQL is a query language used for requesting data from an API in a shape the client defines. It was developed internally by Facebook in 2012 to reduce the number of API calls in their mobile applications. In 2015 they open-sourced it and shared their invention with the public.

When is it worth considering?

The biggest advantage of GraphQL is that it allows the client to choose exactly what they want – is it only a list of book titles to show what’s available or is it a full report on who wrote the book, when, where and how many copies are in stock right now.

It allows the client to fetch nested data in one request. You no longer need to ask for a list of books and then a list of their authors. It can all fit into a single query.

Another advantage is that it provides only one endpoint, usually /graphql, instead of many REST routes, especially when they’re versioned. It makes frontend-backend communication easier, as the frontend no longer needs to know which route to call.

This also allows our application to serve as a proxy for many different backends like REST APIs, databases and gRPC services. This way the client doesn’t need to know what’s underneath the one endpoint that they query.

It can speed up the development process, since frontend teams can freely choose the fields already available in the schema without waiting on the backend to build a new endpoint. That flexibility only goes as far as the schema already exposes, though: if a screen needs a field that doesn’t exist yet, someone still has to add it to the schema and implement it on the backend.

And finally, GraphQL has built-in real-time support, so there’s no need to invent a separate protocol alongside the existing API.

When will it just complicate your life?

GraphQL’s single endpoint can be a disadvantage too, when it comes to caching. In REST, caching comes quite naturally through HTTP-level caching, as a GET request’s URL naturally works as a cache key for CDNs and browsers. Unfortunately, GraphQL loses this advantage, so caching has to beJdeliberately built at other layers instead: on the client side with normalized caches, on the server using persisted queries or custom response caching, inside resolvers, or at the data-source level.

Fetching data from a database requires a little bit more caution, as ill-considered resolvers can trigger a lot of database calls – one for each nested field per parent item.

It’s easier to overload the server, as the shape of a query is entirely up to a client. The application needs to limit the depth and complexity of queries or implement other safeguards that are not necessary in REST. It is also worth considering disabling or restricting schema introspection in production, as it could expose the entire schema to anyone who asks. This only affects discoverability though and doesn’t replace the authorization and query-cost safeguards.

GraphQL doesn’t follow HTTP status codes, as a lot of its responses return 200 OK, even when the operation logically failed – errors can be a part of the response’s body.

Basic concepts

Having a set of rules usually helps in good communication, but they matter especially when it comes to applications. They need to be told what they can expect and what is expected of them, as they cannot figure it out by themselves. When two applications want to talk to each other, the set of rules has to be very clear. In GraphQL we call such a set a schema.

GraphQL: Schema

The schema defines what data is available to get from the application and how it can be queried by another app. It might be written in SDL (Schema Definition Language) or created in a code-first approach. Like any self-respecting language, the schema needs words and sentences. Words are defined in the SDL by using types. When you have word definitions, you can use those words to create sentences that are called queries.

So, when you want to define what a book is, you need to decide on what will describe a book in your application. Usually, books have a title and at least one author with a name, right?

# --- SDL ---  
type Book { 
  id: ID! 
  title: String! 
  publicationYear: Int 
  authors: [Author!]! 
} 
 
type Author { 
  id: ID! 
  name: String! 
} 

With this definition, the schema will know that a Book has: 

  • an id of type ID
  • title that is a String
  • publicationYear that is an Int
  • a list of authors that contain objects called Author

As of now, authors do not need more than: 

  • an id of type ID
  • name that is a String

Let’s dive into the syntax for a moment, as there are a couple of things worth noting. 

  • GraphQL provides a set of built-in scalars that represent primitive values. Aside from well-known primitives like Int, FloatBoolean or String, the ID deserves special attention. It is serialized in the same way as a String, but it signals semantic intent rather than a different format. 
  • When a type has the ! modifier, then it means that it is required and cannot be null
  • When a type is surrounded in [square brackets], then it means it’s a list. A [list]! can be empty but cannot be null

GraphQL: Query

Unfortunately, our two type definitions are not enough. We still need the only type that is required in the GraphQL schema – the Query type. It is the entry point for getting our data and most tools will simply refuse to build the schema and throw a validation error if it’s missing. We can define our Query like this: 

# --- SDL ---  
type Query { 
  book(id: ID!): Book 
  books: [Book!]! 
} 

This means that our application will be able to: 

  • provide information about a single Book identified by an id
  • return a list of all books

Query is one of the three root operation types available in SDL. Each one of them is singular, which means that there can only be one definition of such type in the schema. 

Now we have a schema that’s complete enough to query it. So let’s create the first sentence in our newly defined language: 

# --- request ---  
query GetBook { 
  book(id: "1") { 
    title 
    authors { 
      name 
    } 
  } 
} 

Translated to English, this would be “Please give me the title and the list of authors’ names for a book with the id “1”“. In more technical terms, we’re querying a selection ({...}) of fields: title and names of authors for a book that is identified by the id argument with value "1"

It will be enough to get an answer from our app, that might look like this: 

# --- response ---  
{ 
  "data": { 
    "book": { 
      "title": "Good Omens", 
      "authors": [ 
        { 
          "name": "Terry Pratchett" 
        }, 
        { 
          "name": "Neil Gaiman" 
        } 
      ] 
    } 
  } 
} 

The answer is usually formatted as a JSON, with a few very specific deviations. 

GraphQL: Mutations

We know now how to ask for a book and its authors, so allow us to play with it and do something. That’s what mutations in GraphQL are for – we can create new things, modify them and delete them, when they’re no longer needed.

Let’s get creative and write a book, shall we? We’d need a title and the author’s name.

# --- SDL ---  
input WriteBookInput { 
  title: String! 
  authorName: String! 
} 

As GraphQL clearly distinguishes between types that leave the server and the ones that enter it, when you want to pass a larger or structured argument to the server you cannot use type type, you need to use an input one.

With this WriteBookInput, we’ll provide:

  • title that is a String
  • name that is also a String

We have our data, but we still need action. 

# --- SDL ---  
type Mutation { 
  writeBook(writeInput: WriteBookInput!): Book! 
} 

Here we’ve introduced another one of the three root operation types mentioned previously: Mutation. With this definition it allows us to writeBook, when we provide the writeInput argument and will return the newly written Book to us. Just like with queries, we still get to choose which fields come back in the response. A mutation doesn’t just perform an action; it also lets us select exactly what we want in the response. 

Now that we have all the information we need to write a book, we just have to do it! 

# --- request ---  
mutation WriteBook { 
  writeBook( 
    writeInput: { 
      title: "World of GraphQL", 
      authorName: "Jane Doe" 
    } 
  ) { 
    id 
    title 
    publicationYear 
  } 
} 

As we’re not asking for data, but telling the API it has to do something, this request in English could be translated into “Please write a Book with the title "World of GraphQL" and the author’s name "Jane Doe" and return it’s id, title and publicationYear“. 

Our API will get this request, do it’s magic and return the answer we’ve asked for:  

# --- response ---  
{ 
  "data": { 
    "writeBook": { 
      "id": "2", 
      "title": "World of GraphQL", 
      "publicationYear": 2026 
    } 
  } 
} 

For this specific book we got: 

  • the id, in our case, is just an auto-incremented number; but since GraphQL doesn’t specify how an ID is generated, it could be a UUID or something else entirely 
  • the title, provided by us in the mutation 
  • the publicationYear, that’s just a current year, as we’ve just published our book.

We can extend our Mutation with methods to edit Book‘s data or to remove a Book entirely. The method stays the same. Remember, Mutation is a root type, which means that we cannot declare another one, we have to extend the existing one, by using the extend keyword. 

# --- SDL ---  
input AddAuthorInput { 
  bookId: ID! 
  authorName: String! 
} 

extend type Mutation { 
  addAuthor(addAuthorInput: AddAuthorInput!): Book! 
  deleteBook(bookId: ID!): Boolean! 
} 

# --- request ---  
mutation AddAuthor { 
  addAuthor( 
    addAuthorInput: { 
      bookId: "2", 
      authorName: "Olivia Smith" 
    } 
  ) { 
    authors { 
      name 
    } 
  } 
  deleteBook(bookId: "1") 
} 
 
# --- response ---  
{ 
  "data": { 
    "addAuthor": { 
      "authors": [ 
        { 
          "name": "Jane Doe" 
        }, 
        { 
          "name": "Olivia Smith" 
        } 
      ] 
    }, 

    "deleteBook": true 
  } 
} 

GraphQL: Subscriptions

What if we have our favourite author and we cannot wait for her newest books? We’d love a notification every time she publishes something. Or what if we are book worms, eager to read anything that gets published, as soon as it’s out in the world? That’s what our last root type Subscription is for. 

# --- SDL ---  
type Subscription { 
  bookPublished: Book! 
  bookPublishedByAuthor(authorName: String!): Book! 
} 

This way we can ask the server to notify us whenever any book gets published (bookPublished) or whenever a book from a specific author hits the market (bookPublishedByAuthor). Usually, the connection is established over WebSockets, but as GraphQL does not specify how those notifications should be delivered, that choice is left to the server implementation. When a subscription is set up, it notifies us every single time a book gets written, for as long as we stay subscribed. 

If our friend really likes our work, they can subscribe to notifications for our books like this: 

# --- request ---  
subscription OnNewBookByAuthor { 
  bookPublishedByAuthor(authorName: "Jane Doe") { 
    title 
  } 
}

Such a request could not really be translated into one sentence in a conversation. It’s more like an invitation, “Please tell me, from now on, whenever a new book by “Jane Doe” is published”. 

This way, each time we publish something, our friend will get a notification: 

# --- response ---  
{ 
  "data": { 
    "bookPublishedByAuthor": { 
      "title": "World of GraphQL" 
    } 
  } 
} 

GraphQL: Errors 

Not everything always goes our way. Sometimes the API fails and needs to inform us about what has happened. A REST API relies on HTTP status codes for that, but GraphQL takes a different approach. Errors like incorrect or unauthorized requests, or server crashes, happen outside GraphQL’s own execution, so for those we get 4xx or 5xx as a response. Once a query starts executing, though, the response is almost always 200 OK, even if an error has occurred. GraphQL signals that something went wrong by including a top-level array called errors in the response instead. Interestingly, errors can occur alongside a data object in the response, if only part of the query failed. 

We can get a similar error, when we’ve asked about a book that the API couldn’t find: 

# --- response ---  
{  
  "data": {  
    "book": null  
  }, 
  "errors": [ 
    { 
      "message": "Book could not be found", 
      "locations": [{ "line": 2, "column": 3 }], 
      "path": ["book"], 
      "extensions": { 
        "code": "NOT_FOUND" 
      } 
    } 
  ] 
} 

The structure of errors is defined by the GraphQL specification: 

  • message – the only required field, usually with a description of what went wrong
  • locations – an optional array that stores line and column in the query string where an error has occurred
  • path – an optional array containing information about which field in the response tree the error came from
  • extensions – an optional object for custom values such as error codes, stack trace and other useful metadata.

Summary 

Aside from the basics we’ve covered, GraphQL offers a lot more. You can introduce variables to your queries and reuse them later, and you can query for a field conditionally. You can filter and paginate your data, you can hash your query for better performance or security, you can even merge multiple APIs into one, all depending on how your schema and tooling are set up. And, of course, many, many other functionalities. 

As with every concept, whether it fits your application and architecture or not depends solely on circumstances – how many clients you expect to have, how complex your data is etc. I hope that knowing its basics will help you whenever you decide to try it out and will keep your communication clear and run smoothly 🙂