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:
Schema elements:
CheckCustomDomainAvailability
is the name of the query operation.checkCustomDomainAvailability
is a field on the root Query type in the schema.
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 adomainAvailable
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:
mutation
is the operation type, indicating that this is a mutation rather than a query.AcceptInviteToPublication
is the name of the mutation operation.$input: AcceptInviteToPublicationInput!
is a variable declaration. It specifies that this mutation expects an input of typeAcceptInviteToPublicationInput
, and the!
indicates that this input is required (non-nullable).acceptInviteToPublication(input: $input)
is the actual mutation being called. It's a field on the root Mutation type in the schema, and it takes theinput
argument.success
is a field on the return type of theacceptInviteToPublication
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
andname
of the user, along with their postsid
and title, using pagination (page
andpageSize
).
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: