GraphQL 101: Learn to Build with GraphQL APIs

··

11 min read

Cover Image for GraphQL 101: Learn to Build with GraphQL APIs

Whether you are someone who is getting started with tech or a junior software engineer, APIs are something that you will come across almost every day in your job.

REST and GraphQL APIs are the most widely used ones in the industry, and in this article, we will cover the basics, along with a demo on how you can integrate GraphQL APIs.

TL;DR

Check out the webinar on GraphQL 101, that covers everything we mention in this article:


Understanding how GraphQL works

When working with REST APIs, data is retrieved from specific endpoints. Each endpoint has a defined structure for the information it returns. This means the data requirements of a client are embedded in the URL it connects to.

GraphQL takes a different approach. Instead of multiple endpoints with fixed data structures, GraphQL APIs usually expose only a single endpoint. This is possible because the data structure is not fixed. Instead, it is flexible and allows the client to specify what data is needed.

Key Concepts

  • Schema: Defines the structure of your API, the types of data that can be queried, and how they relate to each other.

  • Fields: Specific pieces of data you request from a type in a query.

  • Queries: The method by which you request specific data.

  • Mutations: Used to modify data (create, update, delete).

  • Types: The building blocks of a schema, representing the shape and structure of data (e.g., String, Int, User, etc.).

Schema and types

A GraphQL server connects the client to data sources, allowing flexible and efficient data queries and manipulations. When a client sends a request, the server uses the GraphQL Schema to validate and execute it, then responds with the data or errors. The server relies on its Schema to handle these requests and define available operations.

Types are the building blocks of a GraphQL schema. They describe the shape of the data that can be queried. A GraphQL Type defines entities with their fields. Here are the different available types:

  • Scalar Type

  • Object Type

  • Input Types

  • Enumeration Type

  • Union and Interface Type

  • Lists and Non-Null

Let us understand it with an example query from Hashnode API, which checks if a custom domain is available or not:

query CheckCustomDomainAvailability($input: CheckCustomDomainAvailabilityInput!) {
  checkCustomDomainAvailability(input: $input) {
    domainAvailable
  }
}

In this example:

  1. Schema elements:

    • CheckCustomDomainAvailability is the name of the query operation.

    • checkCustomDomainAvailability is a field on the root Query type in the schema.

  2. Types:

    • CheckCustomDomainAvailabilityInput! is an input type. The ! indicates that it's a non-nullable type, meaning it's required.

    • There's an implied return type for the checkCustomDomainAvailability field, which includes a domainAvailable field. This return type isn't named in the query, but it exists in the schema.

Mutation

A mutation in GraphQL is an operation used to modify data on the server. While queries are used to read data, mutations are used to create, update, or delete data. Mutations are essential for any operation that changes the state of the data on the server.

Let us understand with an example. The acceptInviteToPublication mutation in Hashnode GraphQL APIs is used to accept an invitation to join a publication.

mutation AcceptInviteToPublication($input: AcceptInviteToPublicationInput!) {
  acceptInviteToPublication(input: $input) {
    success
  }
}

In this example:

  1. mutation is the operation type, indicating that this is a mutation rather than a query.

  2. AcceptInviteToPublication is the name of the mutation operation.

  3. $input: AcceptInviteToPublicationInput! is a variable declaration. It specifies that this mutation expects an input of type AcceptInviteToPublicationInput, and the ! indicates that this input is required (non-nullable).

  4. acceptInviteToPublication(input: $input) is the actual mutation being called. It's a field on the root Mutation type in the schema, and it takes the input argument.

  5. success is a field on the return type of the acceptInviteToPublication mutation, a boolean indicating whether the operation was successful.


GraphQL Integrations with Client

Now that you have an idea of GraphQL schema, types, and mutations, let’s go over several ways to make requests to Hashnode GrapQL API, for example. The same examples used for Hashnode GraphQL API can be used for all GraphQL APIs. Since our client application is mostly built with JavaScript, let’s see how to make Graphql API requests with just Vanilla JavaScript before using URQL a graphql client for the final example.

Basic GraphQL Query

A basic query allows you to fetch specific fields from the GraphQL API. In the example below, we’ll get the user “Favourite“ and these fields name, username, profilePicture, and tagline from the Hashnode Graphql API.

const query = `
  query GetUserProfile {
    user(username: "Favourite") {
      id
      username
      name
      profilePicture
      tagline
    }
  }
`;

fetch('https://gql.hashnode.com/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query,
  })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
  • query: Defined is the GraphQL query operation as a string.

  • fetch: This sends a POST request to the GraphQL endpoint.

  • URL: The Hashnode GraphQL API endpoint (https://api.hashnode.com/).

  • Headers: The Content-Type is set to application/json

  • Body: The request body includes the query serialized into JSON.

Query with Aliases

GraphQL aliases allow you to fetch the same field multiple times with different arguments or rename the results for better clarity. Let’s see how we can achieve this below:

const query = `
   query {
      favUser: user(username: "Favourite") {
        name
      }
      anotherUser: user(username: "Haimantika") {
        name
      }
  }
`

fetch('https://gql.hashnode.com/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query,
  })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
  • From the query favUser and anotherUser are aliases that rename the results, allowing you to query multiple users - (“Favourite“ & “Haimantika“) in one request.

The result of the query would be in this structure, where you have different names of users with one request.

{
  data: {
    favUser: { name: 'Favourite Jome' },
    anotherUser: { name: 'Haimantika Mitra' }
  }
}

Query with Arguments

Arguments are inputs you provide to fields or queries to filter or modify the data you retrieve. They allow you to pass specific parameters to control the query results.

If you didn’t notice, we’ve been using arguments in the previous examples to get the user information from a specific user - “Favourite“. In the example below, we pass two new arguments to a field - posts.

const query = `
  query GetUserWithPosts {
    user(username: "Favourite") {
      id
      name
      posts(page: 1, pageSize: 5) {
        nodes {
          id
          title
        }
      }
    }
  }
`;

fetch('https://gql.hashnode.com/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query,
  })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

Here, page: 1 and pageSize: 5 are arguments passed to the user field, telling the API to return five posts from the user publications on the first page.

Query with Variables

Queries with variables allow you to pass dynamic inputs into a query, making the query flexible and reusable instead of hard-coded like the previous examples. Here’s how to query with variables:

const query = `
  query GetUserWithPosts(
    $username: String!, 
    $page: Int!, 
    $pageSize: Int!
  ) {
    user(username: $username) {
      id
      name
      posts(page: $page, pageSize: $pageSize) {
        nodes {
          id
          title
        }
      }
    }
  }
`;

const variables = {
    username: "Haimantika",
    page: 1,
    pageSize: 5
}

fetch('https://gql.hashnode.com/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query,
    variables
  })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

Explanation:

  • Query Name: GetUserWithPosts is the query name that accepts three variables

  • Variables: The query accepts three variables:

    • $username: String! – a required string for the username.

    • $page: Int! – a required integer to define the page of posts.

    • $pageSize: Int! – a required integer to define the number of posts per page.

  • Query: The query requests the id and name of the user, along with their posts id and title, using pagination (page and pageSize).

Query with Fragments

Fragments allow you to reuse query fields in different parts of the query or across multiple queries. In the example below, we have a fragment named PostDetails, which will be used for the type Post.

const fragment = `
  fragment PostDetails on Post {
    id
    title
    brief
    publishedAt
  }
`;

const query = `
  ${fragment}  # Include the fragment in the query
  query GetUserWithPosts($username: String!) {
    user(username: $username) {
      name
      posts(page: 1, pageSize: 2) {
        nodes {
          ...PostDetails  # Using the fragment here
        }
      }
    }
  }
`;


const variables = {
  username: "Favourite"
};

fetch('https://gql.hashnode.com/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query,
    variables
  })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

To see how fragment shines, in the same query, let’s get the publication details of a blog using the same fragment PostDetails.

const fragment = `
  fragment PostDetails on Post {
    id
    title
    brief
    publishedAt
  }
`;

const query = `
  ${fragment}  # Include the fragment in the query
  query GetUserWithPosts($username: String!) {
    user(username: $username) {
      name
      posts(page: 1, pageSize: 2) {
        nodes {
          ...PostDetails  # Use the fragment here
        }
      }
    }
    publication(host: "blog.favouritejome.dev") {
      id
      title
      post(slug: "why-learn-nextjs-if-i-already-know-reactjs") {
          ...PostDetails # Use the fragment here
      }
    }
  }
`;


const variables = {
  username: "Favourite"
};

fetch('https://gql.hashnode.com/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query,
    variables
  })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

Mutation

As mentioned earlier, mutation operation allows for the modification of data. When creating, deleting or updating action needs to be done, we use a mutation.

With Hashnode API, all mutations require an authentication header to make sure the correct user is updating their data, and you can get your Personal Access Token from the developer settings.

Once you’ve got your Personal Access Token, let’s see how we can create a draft post using the createDraft mutation from Hashnode API.

const mutation = `
  mutation CreateDraft($input: CreateDraftInput!) {
    createDraft(input: $input) {
      draft {
        id
        title
        slug
      }
    }
  }
`;

const variables = {
  input: {
    title: "My New Post",
    slug: 'new-post',
    publicationId: "PUBLICATION_ID",
    contentMarkdown: "This is the content of the post.",
    tags: [],
  }
};

fetch('https://gql.hashnode.com/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `YOUR_PERSONAL_ACCESS_TOKEN` // Replace with your API key
  },
  body: JSON.stringify({
    query: mutation,
    variables
  })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

You need to get the PUBLICATION_ID from your dashboard URL:

Starter Kit Code Example

Now that you have a solid foundation of GraphQL operations, that is, query and mutations, let’s go over the Hashnode starter-kit, to build with GraphQL in production. In the example, we’ll be making use of the hashnode theme.

To follow along, kindly clone the starter-kit repository: https://github.com/Hashnode/starter-kit

We will be using Hashnode’s GraphQL APIs, to list the followers our publication has, and this is how it will look 👇

Let’s start by creating a new file in the queries folder: lib/api/queries/UserFollowers.graphql

query UserFollowers($username: String!, $page: Int!, $pageSize: Int!) {
  user(username: $username) {
    id
    followers(page: $page, pageSize: $pageSize) {
      nodes {
        ...User
      }
    }
  }
}

And the user fragment: lib/api/fragments/User.graphql

fragment User on User {
  id
  username
  profilePicture
  name
  tagline
}

In the pages folder, create a followers.tsx file with the following code:

import request from 'graphql-request';
import { GetServerSideProps } from 'next';
import Head from 'next/head';
import { useEffect, useState } from 'react';
import { useQuery } from 'urql';
import { AppProvider } from '../components/contexts/appContext';
import { Header } from '../components/header';
import { Layout } from '../components/layout';
import {
    PublicationByHostDocument,
    PublicationByHostQuery,
    PublicationByHostQueryVariables,
    PublicationFragment,
    UserFollowersDocument,
    UserFragment,
} from '../generated/graphql';

const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT;

type Props = {
    publication: PublicationFragment;
};

const Followers = (props: Props) => {
    const { publication } = props;

    const [page, setPage] = useState(1);
    const [followersList, setFollowersList] = useState<UserFragment[]>([]); // To append followers
    const [loadingMore, setLoadingMore] = useState(false);

    const [{ data, fetching }] = useQuery({
        query: UserFollowersDocument,
        variables: {
            username: publication.author.username!,
            page: page,
            pageSize: 20,
        },
    });

    // Effect to append new followers when data changes
    useEffect(() => {
        if (data?.user?.followers?.nodes) {
            setFollowersList((prev) => [...prev, ...data?.user?.followers.nodes!]);
            setLoadingMore(false); // Stop loading state after fetching
        }
    }, [data]);

    // Load more function to increment the page
    const loadMore = () => {
        setLoadingMore(true);
        setPage((prevPage) => prevPage + 1);
    };

    return (
        <>
            <AppProvider publication={publication}>
                <Layout>
                    <Head>
                        <title>Followers - {publication.title}</title>
                    </Head>
                    <Header isHome={true} />
                    <div className="feed-width mx-auto my-10 md:w-2/3">
                        <h1 className="mb-5 text-3xl font-bold">Followers</h1>

                        <div className="grid grid-cols-2 gap-2">
                            {followersList?.map((follower) => (
                                <div key={follower.id} className="flex gap-2 bg-slate-400/20 p-3">
                                    <img
                                        src={follower.profilePicture!}
                                        alt={follower.name}
                                        className="h-10 w-10 rounded-full"
                                    />
                                    <div className="flex-1">
                                        <h2 className="font-bold">{follower.name}</h2>
                                        <p>{follower.username}</p>
                                    </div>
                                </div>
                            ))}
                        </div>

                        {/* Load More Button */}
                        <div className="mt-5 text-center">
                            {!fetching && (
                                <button
                                    onClick={loadMore}
                                    className="bg-primary-600 rounded-full  px-4 py-2 text-white"
                                    disabled={loadingMore}
                                >
                                    {loadingMore ? 'Loading...' : 'Load More'}
                                </button>
                            )}
                        </div>
                    </div>
                </Layout>
            </AppProvider>
        </>
    );
};

export default Followers;

export const getServerSideProps: GetServerSideProps = async () => {
    const data = await request<PublicationByHostQuery, PublicationByHostQueryVariables>(
        GQL_ENDPOINT,
        PublicationByHostDocument,
        {
            host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
        },
    );

    const publication = data.publication;
    if (!publication) {
        return {
            notFound: true,
        };
    }

    return {
        props: {
            publication,
        },
    };
};

Resources

If you have made it this far, we hope you have not only learned the basics but also how to integrate GraphQL APIs into your code.

Here are some resources to help you learn more: