We use APIs for practically everything today. They allow us to separate our backend from our frontend, enabling teams to work independently. APIs are also a way to expose data publicly, as many transit authorities and platforms like GitHub do.

API stands for Application Programming Interface - it’s simply an interface that allows users to interact with data. To put it in simple terms, imagine ordering at a restaurant: you choose your meal from a menu and ask the server to bring it to you. In this example, the menu represents your API documentation, the server represents the API, and your request to the server represents the HTTP request to the API.

Building an API itself isn’t that complicated. The complexity lies in making it understandable for developers who will use it, whether they’re internal to the company or external. The API must be well documented and intuitive - which unfortunately isn’t always the case.

The most widely used architectural style today for API development is REST. Unfortunately, REST principles don’t always meet the needs of modern web and mobile applications. When developing a REST API, you’ll encounter certain issues that we’ll discuss right now.

Limitations of REST APIs

Multiple Endpoints

You’ll have multiple endpoints (entry points to your API):

GET /books    # List of books
GET /books/2  # Book with id=2
PUT /books/1  # Update book id=1

N+1 Problem

This problem occurs when we don’t have all the necessary data to display the UI (User Interface). For example, if we want to display a list of books with details for each one, we make a GET /books request and receive this response:

{
  "books":[
    {
      "id":1,
      "isbn":523685941
    },
    {
      "id":2,
      "isbn":845236952
    },
    {
      "id":3,
      "isbn":986532671
    }
  ]
}

We don’t have enough detail about the books - just the id and isbn. So for each book, we must make another HTTP call to get the details. This quickly becomes very resource-intensive.

You might say “Why not add more details to GET /books?” and I’d answer that this is what’s done today, except it easily leads to the third issue I’ll discuss.

Over-fetching

Over-fetching occurs when we retrieve more data than necessary. The more data we add to an endpoint to avoid making multiple API calls, the larger the response becomes, and the more users end up with data they don’t need.

There are obviously solutions to work around these limitations, but they’re often tedious to implement. GraphQL was designed to address these exact problems.

GraphQL

GraphQL is a query language developed by Facebook and used internally since 2012. It has been open-source since 2015.

GraphQL allows you to develop APIs and was designed specifically to address REST’s issues. We’ll see, with some practice, the advantages of GraphQL. For more information, visit the official GraphQL website at graphql.org.

What we’re going to do together is create a GraphQL server and client to make queries. There are many platforms that allow us to do this. For this tutorial, we’ll focus on implementing our Queries and Mutations (don’t worry, I’ll explain these below) rather than on server configuration. For this, we’ll use Graphpack, which allows you to create a GraphQL server with minimal configuration.

Enough theory - let’s dive into the practical part.

Let’s Start

Nothing beats practice for understanding a new concept. I’ve made the project available on GitHub. To follow this tutorial step by step, clone the project and navigate to the start folder. For the impatient among you, the final version is in the final folder.

Let’s look at the project structure together:

root
|───datasources
|    |─characters.js
|───src
|    |─resolvers.js
|    |─schema.graphql
|───package.json

In the package.json, I simply followed Graphpack’s documentation for dependencies and scripts.

Let’s talk about the other project files:

  • schema.graphql: This is where we’ll define our models and the different types of queries we’ll expose.
  • resolvers.js: Resolvers roughly serve to map the schema to the code - for example, specifying that a certain query will execute certain logic.
  • The datasources folder will contain our data sources

I can see your impatience through the screen! To make you wait a bit, how about a little “Hello world!” to start?

Hello World!

The first thing to do is modify our schema.graphql. We’ll create a hello query that returns a simple string. For this, GraphQL has its own schema language (SDL — Schema Definition Language), which is extremely simple to use and understand and is completely framework-agnostic. We’ll discuss this in more detail later. Let’s write:

schema.graphql:

type Query {
    hello: String
}

Now we need to define what the hello() query should return, and this is where resolvers come into play:

resolvers.js:

const resolvers = {
    Query: {
        hello: () => "Hello world!"
    }
}
export default resolvers;

That’s it! Now just launch the server:

npm i # if you haven't already
npm run dev
# ...
# 🚀 Server ready at http://localhost:4000

I’ll let you test your query - you’ll see it’s quite intuitive. Meanwhile, I’ll give you a briefing on the client side.

The GraphQL client interface typically has three main sections:

  1. Query Editor: This is where we’ll define our queries
  2. Response Panel: The response from our call will be displayed here
  3. Documentation Explorer: In this area, you’ll find the description of our schema - basically our documentation

Now that we’ve done our Hello World, let’s move on to the main topic: building a GraphQL API. As a data source, we’ll use a REST API that returns a list of Game of Thrones characters.

Game Of Thrones API

HTTP Client Configuration

First, we’ll install an HTTP client to make requests to the REST API. I opted for axios. Navigate to the project (start folder) and run this command: npm i axios. The dependency will be automatically added to your package.json.

Now let’s create a JavaScript file at the root of the datasources folder, which we’ll name gameOfThronesApi.js:

root
|───datasources
|    |─gameOfThronesApi.js

This file will allow us to configure our HTTP client once and reuse it for all calls to this API.

Let’s move on to the configuration. Add this code to gameOfThronesApi.js:

// # gameOfThronesApi.js

const axios = require('axios');

module.exports = axios.create({
    baseURL: "https://api.got.show/api"
});

We’ve just defined the baseURL. We can also configure other things, but that’s not the goal of this tutorial. Feel free to read the axios documentation if you want to learn more.

Now that we’ve configured our client, we can retrieve data. Let’s implement a DAO that will allow us to do this.

DAO Implementation

We’ll modify the characters.js file to implement data access methods and map the return object to an internal object:

// # characters.js

const gameOfThronesApi = require('./gameOfThronesApi');

export default class Characters {

    async getAllCharacters() {
        const characters = await gameOfThronesApi.get('/characters');

        return Array.isArray(characters.data) 
            ? characters.data.map(character => this.characterReducer(character)) 
            : [];
    }

    characterReducer(character) {
        return {
            id: character._id || 0,
            male: character.male,
            house: character.house,
            slug: character.slug,
            name: character.name,
            books: character.books || [],
            titles: character.titles
        };
    }
}

Nothing too fancy here. We import our HTTP client gameOfThronesApi, and in the getAllCharacters() method, we make a get request to the /characters endpoint. Then we transform the return object into an internal object that will be used later.

So now we have our data source. All that remains is to make it available through GraphQL. To do this, we’ll start by updating our schema.

Updating the GraphQL Schema

In our schema, we’ll define our output objects and their types using GraphQL SDL:

type Query {
    characters: [Character]
}

type Character {
    id: ID!
    male: Boolean
    house: String
    slug: String
    name: String
    books: [String]
    titles: [String]
}

Here we have two distinct parts:

  • type Query: This is where we’ll define our queries, and these queries will be declared in our resolvers. Here, the characters query returns a list of type Character ([Character]). This type is defined below.
  • type Character: This is a GraphQL object type we created, composed of fields with standard object types. The only somewhat unusual type is ID! - it defines the unique identifier of the object. The ! means the object cannot be null. To go further, I recommend reading the GraphQL schema documentation.

Now that we have data sources and the output object format, we just need to implement our resolvers to “map” the query to the data.

Implementing Resolvers

Each resolver accepts 4 arguments:

fieldName: (obj, args, context, info) => result
  • obj: The object containing the result returned by the parent resolver (we won’t use it here).
  • args: An object containing the arguments passed in the query. For example, characters(name: "Aegon") - the args object would have the value {"name": "Aegon"}.
  • context: An object shared by all resolvers; it can contain, for example, authentication information.
  • info: Contains information about the execution status of the query.

For now, we won’t use any of these arguments, so we’ll use empty parentheses:

// # resolvers.js

import Characters from './datasources/characters';

let characters = new Characters();

const resolvers = {
    Query: {
        characters: async () => characters.getAllCharacters(),
    }
}
export default resolvers;

Here we’re saying that for the characters query, execute the characters.getAllCharacters() function. Simple as that.

Let’s restart the server:

npm run dev
# ...
# 🚀 Server ready at http://localhost:4000

You can test your queries using autocompletion. You’ll notice that the GraphQL client requires you to enter exactly the fields you need - this avoids over-fetching.

You’ll also see that we receive exactly what we ask for, and we can go a bit further by changing the names of the return attributes. Try this query, for example:

query {
  characters {
    nom: name
    livres: books
  }
}

Here’s the response received:

{
  "data": {
    "characters": [
      {
        "nom": "Abelar Hightower",
        "livres": [
          "The Hedge Knight"
        ]
      },
      {
        "nom": "Addam Frey",
        "livres": [
          "The Mystery Knight"
        ]
      }
      // ...
    ]
  }
}

Now let’s see how to pass arguments in our query. What if we want to search by name? First, we’ll implement the method in characters.js:

// characters.js
async getCharacterByName(name) {
    const characters = await gameOfThronesApi.get(`/characters/${name}`);
    return this.characterReducer(characters.data.data);
}

Then we’ll modify our GraphQL Query as follows:

// schema.graphql
type Query {
    characters: [Character]
    character(name: String!): Character
}

We simply pass a non-null argument of type String to our query, and it returns an object of type Character.

Finally, and this is the most interesting part, we’ll add a resolver in resolvers.js:

// resolvers.js
const resolvers = {
    Query: {
        characters: async () => characters.getAllCharacters(),
        character: async (parent, {name}) => characters.getCharacterByName(name)
    }
}

We put our argument in the second position in our argument list, and we’re done! We can make our queries:

query {
  character(name: "Aegon Targaryen") {
    name
    male
    titles
  }
}

And we receive the response:

{
  "data": {
    "character": {
      "name": "Aegon Targaryen (son of Aenys I)",
      "male": true,
      "titles": [
        "Prince"
      ]
    }
  }
}

Note that the API we’re using doesn’t actually perform a real name search - it returns the first character in the list, but that’s not the point of this tutorial.

We’ve seen what a GraphQL Query is - it allows us to retrieve data (the equivalent of GET in REST). What if we want to modify data? Well, we use what are called Mutations.

Mutations

We’ll create a houses.js file in the datasources folder that will contain a (small) list of Game of Thrones houses:

let houses = [
    { id: 1, name: "House Stark", words: "Winter is Coming" },
    { id: 2, name: "House Targaryen", words: "Fire and Blood" },
    { id: 3, name: "House Baratheon", words: "Ours is the Fury" },
    { id: 4, name: "House Greyjoy", words: "We Do Not Sow" },
];

export default houses;

Then we’ll update the schema to define queries that will allow us to modify data in this list:

// schema.graphql
type Query {
    characters: [Character]
    character(name: String!): Character
    houses: [House]
}

type Mutation {
    """
    Create a new house
    """
    createHouse(id: ID!, name: String!, words: String): [House]!

    """
    Update an existing house
    """
    updateHouse(id: ID!, name: String!, words: String): [House]!

    """
    Delete a house
    """
    deleteHouse(id: ID!): [House]!
}

type House {
    id: ID!
    name: String
    words: String
}

You’ll notice I added lines surrounded by """ - this is to document our API. You’ll see this description in the GraphQL client’s “Schema” section.

Now that everything is ready, let’s implement our resolvers:

// resolvers.js
import houses from './datasources/houses';

const resolvers = {
    Query: {
        characters: async () => characters.getAllCharacters(),
        character: async (parent, {name}) => characters.getCharacterByName(name),
        houses: () => houses
    },
    Mutation: {
        createHouse: (parent, {id, name, words}) => {
            let newHouse = {id, name, words};
            houses.push(newHouse);
            return houses;
        },
        updateHouse: (parent, {id, name, words}) => {
            let houseToUpdate = houses.find(house => house.id == id);
            if (!houseToUpdate) {
                throw new Error("House not found.");
            }
            houseToUpdate.name = name;
            houseToUpdate.words = words ? words : houseToUpdate.words;
            return houses;
        },
        deleteHouse: (parent, {id}) => {
            let houseIndex = houses.findIndex(house => house.id == id);
            
            if (houseIndex === -1) {
                throw new Error("House not found.");
            }
            
            houses.splice(houseIndex, 1);
            return houses;
        }
    }
}

export default resolvers;

We’ve created a new Mutation attribute that contains our resolvers with the arguments we’ll define as input.

Let’s launch our server (npm run dev) and see what this looks like with the following query:

mutation {
  createHouse(id: 5, name: "House SOAT", words: "In sharing we trust") {
    name
    words
  }
}

Result:

{
  "data": {
    "createHouse": [
      {
        "name": "House Stark",
        "words": "Winter is Coming"
      },
      {
        "name": "House Targaryen",
        "words": "Fire and Blood"
      },
      {
        "name": "House Baratheon",
        "words": "Ours is the Fury"
      },
      {
        "name": "House Greyjoy",
        "words": "We Do Not Sow"
      },
      {
        "name": "House SOAT",
        "words": "In sharing we trust"
      }
    ]
  }
}

Conclusion

We’ve learned how to create a GraphQL API, use Queries to retrieve data, and Mutations to modify data.

To go further, I recommend exploring modern GraphQL implementations like Apollo Server, which provides a robust and production-ready GraphQL server. You can also explore GraphQL client libraries like Apollo Client or Relay for frontend applications.

GraphQL has evolved significantly since its open-source release and has become a mature technology adopted by major companies worldwide. Its ability to solve common REST API problems - like over-fetching, under-fetching, and the N+1 problem - makes it an excellent choice for modern API development.

Key takeaways:

  • GraphQL provides a flexible query language that lets clients request exactly the data they need
  • Schemas provide strong typing and auto-generated documentation
  • Mutations handle data modifications in a structured way
  • The resolver pattern cleanly separates business logic from the API layer

Happy coding with GraphQL!