After this tutorial, you will be able to:
- Create your own GraphQL schema
- Serve a backend API for data fetching
- Start a simple react GraphQL client
- Fetch data!
- Introduction
- Getting Started
- Basic Syntax
- GraphQL Playground
- Queries, Mutations and Subscriptions
- React-Apollo
- Wrapping Up
- References
Before we start, let me give you an brief introduction to GraphQL. GraphQL is a query language for API's. It is a more efficient alternative to traditional RESTful API's since it is:
- Faster - with user-defined specification for data fetching,, GraphQL prevents clients from underfetching or overfetching, making network requests more efficient.
- More Flexible - user can define their own schemas and data types to share between frontend and backend.
- Faster Production - with schemas acting as a contract for data fetching between the frontend team and backend team, both teams can do their individual work without further communication.
An example would be: Say you are creating an Instagram-like app. When a user opens up the app, you want to show him the posts on his news feed, so you make a network request to your backend and fetch all the post data. As for the traditional RESTful API, you fetch the post data including the comments and details about the comments on the posts. However, that would be overfetching since the user may not necessarily click into individual posts to check comments. Here, GraphQL comes into play by letting you specify what kind of data you want and how many comments you want for each post. This not only limits the amount of data needed to transfer through the internet, it also speeds up the fetching efficiency and speed.
In this tutorial, I will teach you how to set up your own GraphQL API, write a simple GraphQL client, and start communication between backend and frontend.
I will be using graphql-yoga for the backend service, and apollo for a simple client side.
Type the following steps into terminal to get started with this tutorial.
git clone https://github.com/ian13456/modern-graphql-tutorial.git
cd modern-graphql-tutorial
npm install
I will walk you through each file later on and teach you what it's for.
One awesome tool that comes with graphql-yoga is GraphQL Playground. GraphQL Playground allows us to test our schemas without having to set up a client side. Essentially, it serves as a GraphQL client to run queries and mutations on.
After cloning the repository, run npm start
and navigate to http://localhost:4000 Don't worry. At this point it is normal to have zero clue about what the playground is all about. Pasted below is the process of setting up a simple GraphQLServer and serving it on port 4000. Disregard the imports as they will be discussed later on. All you have to know is that the following section of code sets you up with a working GraphQLServer.
// backend/src/index.js
import { GraphQLServer, PubSub } from 'graphql-yoga'
import db from './db'
import Query from './resolvers/Query'
import Mutation from './resolvers/Mutation'
import Subscription from './resolvers/Subscription'
import User from './resolvers/User'
import Post from './resolvers/Post'
import Comment from './resolvers/Comment'
// This will be covered later.
const pubsub = new PubSub()
// Don't worry about anything; just know that this initializes the server.
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers: {
Query,
Mutation,
Subscription,
User,
Post,
Comment
},
context: {
db,
pubsub
}
})
// Serving server on port 4000
server.start({ port: process.env.PORT | 4000 }, () => {
console.log(`The server is up on port ${process.env.PORT | 4000}!`)
})
As mentioned before, GraphQL is a query language with specific syntax.
Here an example of a basic query:
query {
users {
username
password
creditCardNumber
}
}
As I said before, GraphQL gives you the ability to specify what data's are needed for fetching.
Lets start from the beginning. The out-most layer query { }
specifies that the type of this GraphQL request is a query, meaning that it is simply asking for data.
On the second layer, we have the specific query to run called users { }
. This tells the server which kind of data the client wants, and lets the server know what to do accordingly.
Lastly, within the users
query we have the specific attributes that we are fetching for EACH user, in this case we are trying to fetch the username
, password
and credit card number
.
A GraphQL Schema is what defines the basic types. The file is often named schema.graphql
, sitting in the same directory as index.js. You add it into the server with the typedefs
option in graphql-yoga.
These are the basic scalar types in GraphQL. Scalar types basically mean that these values are scalars and do not contain any sub-fields below them.
- String
- Int
- Float
- Boolean
- ID
To add on, GraphQL "Lists" is a type that defines an array of a certain type with the notation []
. For example: [String]
represents a type that is a list of Strings.
For other special types such as queries
, mutations
and subscriptions
, I will go deeper on how to define them once I explain what they are later on.
Queries, mutations and subscriptions are the three major types that you include in schema.graphql
that define what the GraphQL requests should look like. Essentially they differ between different HTTP request types for each GraphQL request.
Before I dig into what these three types are, I want to teach you about what GraphQL resolvers are for first.
Resolvers are methods that you write in any language that defines what each query, mutation, and subscription does. They basically tell the GraphQL endpoint how to handle different types of data fetches. In most cases, resolvers are where you alter the database after receiving data from GraphQL requests. In graphql-yoga, they are the files imported and included in the resolvers
option in the new GraphQLServer
.
With the knowledge of schemas
and resolvers
, we can now learn the top three request types in GraphQL.
These are the types of network requests that simply ask for data from the server.
Remember the example I showed you in the Basic Syntax section?
Here I defined the exact same query type users
that I showed you.
# backend/src/schema.graphql
type Query {
users(query: String): [User!]!
}
type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
comments: [Comment!]!
}
There are three important keys that I want to point out in this short GraphQL code:
type Query
v.s.type User
- Remember "Query" is the type of the outer-most layer in a GraphQL request, and "User" in this case is just a user-defined type that can be used in the entire schema.
!
Exclamation marks after type names!
in GraphQL means non-nullable, meaning that the data sent back cannot be null or undefined. For example,[String!]!
represents a non-nullable list containing values of Strings that cannot be null.
(query: String)
afterusers
- Just like functions, GraphQL requests can have additional arguments. In this case the query
users
takes inquery
of typeString
. Notice how there isn't a!
afterString
. This means that the argumentquery
is optional.
- Just like functions, GraphQL requests can have additional arguments. In this case the query
However, this only tells GraphQLServer what the query looks like though. So, below is where I use resolvers to define HOW the server should handle any users
requests.
// backend/src/resolvers/Query.js
users(parent, args, context, info) {
const { db } = context
// Check if the optional `query` of type String is passed in.
if (!args.query) {
return db.users
}
// Reaching here means `query` is defined -> filter the data passed back.
return db.users.filter((user) => {
return user.name.toLowerCase().includes(args.query.toLowerCase())
})
}
Here's a possible GraphQL request for this users
query:
query {
users(query: "a") {
id
name
age
}
}
# results:
{
"data": {
"users": [
{
"id": "1",
"name": "Andrew",
"age": 27
},
{
"id": "2",
"name": "Sarah",
"age": null
}
]
}
}
Now that you have a basic understanding of what queries are, Mutations are fairly easy to understand. Mutations are where you mutate the data in the database. note that I used a temporary file db.js
as a database since the primary focus of this tutorial is on GraphQL not on databases.
# backend/src/schema.graphql
type Mutation {
createUser(data: CreateUserInput!): User!
}
input CreateUserInput {
name: String!
email: String!
age: Int
}
There are only a few things worth noticing in this Mutation type specification:
type Mutation
v.s. previoustype Query
- Later on when making GraphQL requests, use
mutation { }
instead ofquery { }
- Later on when making GraphQL requests, use
- new keyword
input
- This is used for argument specifications. When arguments become long and nasty, it's best to extract them and define an input type.
Ok, with the createUser
mutation specified above, all we're missing is the resolver corresponding for it โ
// backend/src/resolvers/Mutation.js
createUser(parent, args, context, info) {
const { db } = context
// Check if email is already taken in fake database
const emailTaken = db.users.some((user) => user.email === args.data.email)
if (emailTaken) {
throw new Error('Email taken')
}
// Create new user object
const user = {
id: uuidv4(),
...args.data
}
// Save (append) it to fake database
db.users.push(user)
return user
}
Worth noting that in a regular project, the lines of code where I alter the fake database should be replaced by processes that communicate with an actual database and change/save/delete data.
Here's an example of a GraphQL mutation:
mutation {
createUser(data: {
name: "Morgan"
age: 81
email: "[email protected]"
}) {
id
name
}
}
# results:
{
"data": {
"createUser": {
"id": "47afd124-0c06-4e2e-992f-0b837d4f0d5e",
"name": "Morgan"
}
}
}
Subscriptions are a little bit different from regular Queries and Mutations. This is a very interesting feature provided by GraphQL that handles the real time aspect of modern web.
Think of GraphQL Subscriptions as YouTube subscriptions. Once you are subscribed to YouTube channel, you get notified with the latest news from the channel. Not only you, it's everyone who's subscribed gets notified.
With GraphQL Subscriptions, you can set up quote unquote "channels" on your endpoint with specified types of "content" that these "channels" post. Once data is mutated, you can then "post" the data up to the channel, notifying whoever's subscribed. Here's an example in my code of subscriptions on the comment section of a specific post:
# backend/src/schema.graphql
type Subscription {
comment(postId: ID!): CommentSubscriptionPayload!
}
type CommentSubscriptionPayload {
mutation: MutationType!
data: Comment!
}
enum MutationType {
CREATED
UPDATED
DELETED
}
There are only two important key points I want to mention here:
type Subscription
- New type! use
subscription { }
instead ofmutation { }
for outer-most layer.
- New type! use
- new keyword
enum
- An enum specifies a set of
Strings
that you can use throughout your schema. - When you define an Enum, you are basically defining a set of fixed Strings.
- An enum specifies a set of
The resolver of GraphQL subscriptions is a bit trickier. Since you are now not only querying or mutating data, but actually setting up a websocket link between the client and the server, you need pubsub system on the server-side.
Looking back to index.js, we can now clearly see the initialization of a PubSub
instance. We then pass it into context
option in the GraphQLServer.
// backend/src/index.js
import { GraphQLServer, PubSub } from 'graphql-yoga'
// ...import...lots...of...files...
const pubsub = new PubSub()
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers: {
Query,
Mutation,
Subscription,
User,
Post,
Comment
},
context: {
db,
pubsub
}
})
Now you may wonder what the context
option is for. If you look closely to resolver functions of the multiple examples above, you may notice a sequence of weird function arguments parent
, args
, context
and info
. This is where the context
keyword comes in play. By passing both the pubsub
instance and db
into context
, we can use them within our resolvers later on.
However, the PubSub system isn't as easily set up as you thought. In order to set up websocket channels for each GraphQL Subscription, you need to set up a required Subscription
resolver like the one below.
// backend/src/resolvers/Subscription.js
const Subscription = {
comment: {
subscribe(parent, { postId }, { db, pubsub }, info) {
const post = db.posts.find(post => post.id === postId && post.published)
if (!post) {
throw new Error('Post not found')
}
return pubsub.asyncIterator(`comment ${postId}`)
}
}
}
export { Subscription as default }
Here I directly destructure db
and pubsub
out of context
, and extract postId
from the original argument arguments
. I then check if the post exists or not. If it doesn't, I throw an Error to the console. If it does exit, and here comes the tricky part, I create and return a pubsub asyncIterator
with a specific tag of comment ${postId}
. You can think of this with the YouTube subscription example. Creating an asyncIterator
is like creating a new YouTube channel. Later on you can then post data onto pubsub
with the specific channel tag, and the data will be broadcasted to all instances connected.
With the pubsub
system set up, I can now broadcast data throughout all of my resolvers. For example, whenever a comment on a specific post is created, I want to post the comment data onto the comment's individual Subscription Channel. I can do that with the code below:
// backend/src/resolvers/Mutation.js
...
const comment = {
id: uuidv4(),
...args.data
}
// THIS IS WHERE I PUBLISH MY DATA TO THE CHANNEL
pubsub.publish(`comment ${args.data.post}`, {
comment: {
mutation: 'CREATED',
data: comment
}
})
...
To sum up, here's a quick example of how GraphQL subscriptions work:
# subscription:
subscription {
comment (postId: 10) {
mutation
data {
text
author {
name
}
}
}
}
# mutation that was run after subscription:
mutation {
createComment(data: {
text: "Hello sir! Nice Post!"
author: 1
post: 10
}) {
text
author {
name
}
}
}
# results on subscription side:
{
"data": {
"comment": {
"mutation": "CREATED",
"data": {
"text": "Hello sir! Nice Post!",
"author": {
"name": "Andrew"
}
}
}
}
}
In this example, I created an application that contains a form and a list of posts. The form allows you to use GraphQL mutations
to create posts in the database, and the list of posts is fetched with a GraphQL query
. This list is actually subscribed to any new posts, which means when the form creates a new post, the post gets saved in the database and sent back to the client spontaneously through a GraphQL subscription
.
Before I start explaining the magic behind this, I strongly recommend you read the react-apollo documentations. It is well documented, and you will definitely learn a lot from it.
In order to run the live exmaple above, run the commands below:
cd frontend
npm install
npm start
In order to connect to the backend API, you need to set up a GraphQL client like so:
// frontend/src/index.js
import { ApolloClient, InMemoryCache } from 'apollo-boost'
import { ApolloProvider } from 'react-apollo'
import { split } from 'apollo-link'
import { HttpLink } from 'apollo-link-http'
import { WebSocketLink } from 'apollo-link-ws'
import { getMainDefinition } from 'apollo-utilities
// Create an http link:
const httpLink = new HttpLink({
uri: 'http://localhost:4000/'
})
// Create a WebSocket link:
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/`,
options: { reconnect: true }
})
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
const client = new ApolloClient({
link,
cache: new InMemoryCache().restore({})
})
The HTTP link (httpLink
) is for queries and mutations, and the WebSocket link (wsLink
) is for GraphQL subscriptions. With the split
function, we can tell apart the types of GraphQL requests. We then send the request to different endpoints accordingly.
Querying in react-apollo is pretty simple. All you have to do is pass a gql
tagged string, which is exactly like what you would enter in GraphQL Playground, into a Query
component.
// frontend/src/graphql/queries.js
import { gql } from 'apollo-boost'
export const POSTS_QUERY = gql`
query {
posts {
title
body
author {
name
}
published
}
}
`
// frontend/src/graphql/subscriptions.js
export const POSTS_SUBSCRIPTION = gql`
subscription {
post {
mutation
data {
title
body
author {
name
}
published
}
}
}
`
// frontend/src/containers/App/App.js
let unsubscribe = null
// ...
<Query query={POSTS_QUERY}>
{({ loading, error, data, subscribeToMore }) => {
if (loading) return <p>Loading...</p>
if (error) return <p>Error :(((</p>
const posts = data.posts.map((post, id) => (
<Post data={post} key={id} />
))
if (!unsubscribe)
unsubscribe = subscribeToMore({
document: POSTS_SUBSCRIPTION,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev
const newPost = subscriptionData.data.post.data
return {
...prev,
posts: [newPost, ...prev.posts]
}
}
})
return <div>{posts}</div>
}}
</Query>
The function within the Query
tag is defined to be the process you execute once the GraphQL request is fired. data
represents the data received; loading
is a boolean indicating whether the query is pending or not. Once the data is received, you can then return whatever you want to render with the data. One thing to notice is the subscribeToMore
function.
The subscribeToMore
function is what tells the GraphQL client to listen to any updates. Once an update is sent from backend through wsLink to the client, the updateQuery
function in subscribeToMore gets executed. It basically takes the cached posts from the query, prev
, append the freshly received post onto it, and put the prev
cache back in place. (reason to the unsubscribe
variable is explained here.)
Mutating in Apollo is also very simple. The Mutation
tag takes in a gql
tagged mutation, and gives you a function that you can run every time you want to make mutation requests.
// frontend/src/graphql/mutations.js
import { gql } from 'apollo-boost'
export const CREATE_POST_MUTATION = gql`
mutation createPost(
$title: String!
$body: String!
$published: Boolean!
$authorId: ID!
) {
createPost(
data: {
title: $title
body: $body
published: $published
author: $authorId
}
) {
title
body
author {
name
}
published
}
}
`
The variables with names are just variables that you can pass into later on with the variables
option. (you will see it later)
// frontend/src/containers/App/App.js
<Mutation mutation={CREATE_POST_MUTATION}>
{createPost => {
this.createPost = createPost
return (
/** FORM ELEMENT HERE */
)
}}
</Mutation>
I purposely saved the createPost function generated by Mutation
so I can use it later in handleFormSubmit
. Don't forget to check out the source file to have a look at how I created the form.
Last thing to do after setting up the mutation is to fire the mutation every time the form is submitted. This is exactly what handleFormSubmit
does.
// frontend/src/container/App/App.js
handleFormSubmit = e => {
e.preventDefault()
const { formTitle, formBody } = this.state
if (!formTitle || !formBody) return
this.createPost({
variables: {
title: formTitle,
body: formBody,
published: true,
authorId: 2
}
})
this.setState({
formTitle: '',
formBody: ''
})
}
That's it! By now you should be able to:
- Create your own GraphQL API
- Set up GraphQL client and connect the client to backend
- Fetch data and listen to data changes
There are a lot more to learn about GraphQL such as user authentication. There are even tools that automatically generate the necessary mutations for your GraphQL schema such as Prisma!
Anyways, hope this tutorial was helpful. Email me if you have any further questions!