跳到主要内容

Getting up and running with GraphQL

In other words, GraphQL is a declarative data fetching specification and query language. It is meant to provide a common interface between the client and the server for data fetching and manipulations.

From the name GraphQL, you might be tempted to think that GraphQL is related to graph database or SQL. In fact, GraphQL has nothing to do with data storage; nor does it have anything to do with graph databases.

To further understand what GraphQL is, let’s take a look at some of its features:

  • Declarative: With GraphQL, what you queried is what you get, nothing more and nothing less.
  • Hierarchical: GraphQL naturally follows relationships between objects. With a single request, we can get an object and its related objects. For instance, with a single request, we can get an Author along with the Posts he has created, and in turn get the Comments on each of the posts.
  • Strongly-typed: With GraphQL type system, we can describe possible data that can be queried on the server and rest assured that the response we’ll get from a server is in line with what was specified in the query.
  • Not Language specific:** GraphQL is not tied to a specific programming language as there are several implementations of the GraphQL specification in different languages.
  • Compatible with any backend: GraphQL is not limited by a specific data storage; you can use your existing data and code or even connect to third-party APIs.
  • Introspective: With this, a GraphQL server can be queried for details about the schema.

In addition to these features, GraphQL is easy to use as it has a JSON like syntax and also provides lots of performance benefits.

Having seen what GraphQL is and some of its features, let’s dive deeper. We’ll see the syntax and the operations that can be performed with GraphQL. GraphQL operations can be either read or write operations.

Query

Queries are used to perform read operations, that is, fetching data from the server. Queries define the actions we can perform on the schema (more on schema shortly). Below is a simple GraphQL query and its corresponding response:

# Basic GraphQL Query

{
author {
name
posts {
title
}
}
}

# Basic GraphQL Query Response

{
"data": {
"author": {
"name": "Chimezie Enyinnaya",
"posts": [
{
"title": "How to build a collaborative note app using Laravel"
},
{
"title": "Event-Driven Laravel Applications"
}
]
}
}
}

This is a simple query that gets the name of an author and the posts created by the author. Notice that the query and the response have the same structure. Also, the response contains an array of posts created by the author and we were able to achieve this with a single request.

Let’s go over and explain the components of a GraphQL query.

In the query above, we omitted the query keyword. If an operation doesn’t include a type, by default GraphQl will treat such operation as a query. A query can have a name. Though the name is optional, it makes it easy to identify what a particular query does. In production applications, it is recommended to use the query keyword and name your queries so as to make your code easy to understand.

query GetAuthor {

author {

# The name of the author

name

}

}

We have given our query a name GetAuthor which is quite descriptive. Queries can also contain comments. Each line of comment must start with the # sign.

Fields

Fields are basically parts of an object we want to retrieve from a server. In the query above, name is a field on the author object.

Arguments

Just like functions in programming languages can accept arguments, a query can accept arguments. Arguments can be either optional or required. So we can rewrite our query to accept the ID of the author as an argument:

{

author(id: 5) {

name

}

}

Heads up: GraphQL requires strings to be wrapped in double quotes.

Variables

In addition to arguments, a query can also have variables which make a query more dynamic instead of hard coding the arguments. Variables are prefixed with the $ sign followed by their type.

query GetAuthor($authorID: Int!) {

author(id: $authorID) {

name

}

}

Variables can also have default values:

query GetAuthor($authorID: Int! = 5) {

author(id: $authorID) {

name

}

}

Like arguments, variables can be either optional or required. In our example, $authorID is required because of the ! symbol (more on this under schema) included in the variable definition.

Aliases

With the above query, let’s assume we want to get authors with IDs 5 and 7 respectively. We might be tempted to do something like this:

{

author(id: 5) {

name

}

author(id: 7) {

name

}

}

This will throw an error as there would be conflict between the two name fields. To resolve this, we’ll use aliases. With aliases, we can give the fields customised names and request data from the same field with different arguments.

{

chimezie: author(id: 5) {

name

}

neo: author(id: 7) {

name

}

}

And the response will be something similar to:

# Response

{

"data": {

"chimezie": {

"name": "Chimezie Enyinnaya"

},

"neo": {

"name": "Neo Ighodaro"

}

}

}

Fragments

Fragments are reusable set of fields that can be included in queries as needed. Assuming we need to fetch a twitterHandle field on the author object, we can easily do that with:

{

chimezie: author(id: 5) {

name

twitterHandle

}

neo: author(id: 7) {

name

twitterHandle

}

}

But what about if we want to pull more fields? This can quickly become repetitive and redundant. That’s where fragments comes into play. Below is how we would solve the above situation using fragments:

{

chimezie: author(id: 5) {

...authorDetails

}

neo: author(id: 7) {

...authorDetails

}

}

fragment authorDetails on Author {

name

twitterHandle

}

Now we will only need to add our fields in one place.

Directives

Directives provide a way to dynamically change the structure and shape of our queries using variables. As of this post, the GraphQL specification includes exactly two directives:

  • @include will include a field or fragment only when the if argument is true
  • @skip will skip a field or fragment when the if argument is true

The two directives both accept a Boolean (true or false) as arguments.

query GetAuthor($authorID: Int!, $notOnTwitter: Boolean!, $hasPosts: Post) {

author(id: $authorID) {

name

twitterHandle @skip(if: $notOnTwitter)

posts @include(if: $hasPosts) {

title

}

}

}

The server will not return a response with the author’s Twitter handle if $notOnTwitter is true. Also, the server will return a response with the author’s post only if the author has written some posts, that is, $hasPosts is true.

Mutation

In a typical API usage, there are scenarios where we would want to modify the data on the server. That’s where mutations come into play. Mutations are used to perform write operations. By using mutations, we can make a request to a server to amend or update specific data, and we would get a response that contains the updates made. It has a similar syntax to the query operation with a slight difference.

mutation UpdateAuthorDetails($authorID: Int!, $twitterHandle: String!) {

updateAuthor(id: $authorID, twitterHandle: $twitterHandle) {

twitterHandle

}

}

We send data as a payload in a mutation. For our example mutation, we could send the following data as payload:

# Update data

{

"authorID": 5,

"twitterHandle": "ammezie"

}

And we’ll get the following response after the update has been made on the server:

# Response after update

{

"data": {

"id": 5,

"twitterHandle": "ammezie"

}

}

Notice the response contains the newly updated data.

Just like queries, mutations can also accepts multiple fields. An important distinction between mutations and queries is that mutations are executed serially in order to ensure data integrity, whereas queries are executed in parallel.

Schemas

Schemas describe how data are shaped and what data on the server can be queried. Schemas provide object types used in your data. GraphQL schemas are strongly typed, hence all the object defined in a schema must have types. Types allow the GraphQL server to determine whether a query is valid or not at runtime. Schemas can be of two types: Query and Mutation.

Schemas are constructed using what is called GraphQL schema language, which is quite similar to the query language we saw in the previous sections. Below is a sample GraphQL schema:

type Author {

name: String!

posts: [Post]

}

The above schema defines an Author object type with two fields (name and posts). This means that we can only use name and posts fields on any GraphQL query operation on Author. The fields on an object types can be optional or required. The name is required because of the ! symbol after it type name.

Arguments

Fields in a schema can accept arguments. These arguments can either be optional or required. Required arguments are denoted with the ! symbol:

type Post {

allowComments(comments: Boolean!)

}

Scalar Types

Out of the box, GraphQL comes with the following scalar types:

  • Int: a signed 32‐bit integer
  • Float: a signed double-precision floating-point value
  • String: A UTF‐8 character sequence
  • Boolean: true or false
  • ID: represents a unique identifier

Fields defined as one of the scalar types cannot have fields of their own. We could also specify custom scalar types using the scalar keyword. For example, we could define a Date type:

scalar Date

Enumeration types

Also called Enums, are a special kind of scalar that is restricted to a particular set of allowed values. With Enums, we can:

  • Validate that any arguments of this type are one of the allowed values
  • Communicate through the type system that a field will always be one of a finite set of values

Enums are defined with the enum keyword:

enum Media {

Image

Video

}

Input types

Input types are valuable in the case of mutations, where we might want to pass in a whole object to be created. In the GraphQL schema language, input types look exactly the same as regular object types, but with the keyword input instead of type. The input type is defined as below:

# Input Type

input CommentInput {

body: String!

}

Let’s Get Practical

Enough of the theory, let’s put what we’ve learned so far into practice. We’ll be building a simple task manager GraphQL server with Node.js. Like I said, it is going to be simple but it will cover most of the concepts we learned above and help solidify our understanding of them. Below is a demo of what we’ll be building:

image.gif

The complete code is available on GitHub

So let’s get started already.

Setting Up Node.js Server

We’ll be using Express as our Node.js framework. Initialize a new Node.js project by using the following command:

mkdir graphql-tasks-server

cd graphql-tasks-server

npm init -y

I named the demo graphql-tasks-server, but feel free to call it whatever you like. We should have a package.json in the project directory. Next, we’ll install Express and some other dependencies that our app will need:

``` language-javascript

npm install express body-parser apollo-server-express graphql graphql-tools lodash --save

  • Express: Node.js framework.
  • Body-parser: Node.js body parsing middleware.
  • Apollo-server-express: Apollo GraphQL server for Express.
  • Graphql: a reference implementation of GraphQL for JavaScript
  • Graphql-tools: an npm package and an opinionated structure for how to build a GraphQL schema and resolvers in JavaScript.
  • Lodash: a modern JavaScript utility library delivering modularity, performance & extras.

Having installed our dependencies, let’s start writing some code. We’ll create a src directory which will contain all the code we’ll be writing. Within the src directory, create schema and data directory respectively. The schema directory will contain our schemas and resolvers, while the data directory will contain the sample data we’ll be using for the purpose of this tutorial. Within the schema directory, create index.js and resolvers.js files. Within the data directory, create a data.js file. Lastly, create a server.js file directly in the src directory. We should now have a structure like this:

image.png

Open src/server.js and add the code below into it:

// src/server.js

const express = require('express');

const bodyParser = require('body-parser');

const { graphqlExpress, graphiqlExpress } = require('apollo-server-express');

const schema = require('./schema/index');

const PORT = 3000;

const app = express();

// Graphql

app.use('/graphql', bodyParser.json(), graphqlExpress({ schema }));

// Graphiql

app.use('/graphiql', graphiqlExpress({ endpointURL: 'graphql' }));

app.listen(PORT, () => console.log(GraphiQL is running on http://localhost:${PORT}/graphiql));

We pulled in our dependencies (express, body-parser and apollo-server-express) and also our schema (which we’ll create shortly). We have two endpoints: /graphql and /graphiql. The first endpoint is the main endpoint which our GraphQL requests to the server will be made to. We add to it the body-parser middleware as well as Apollo server. Apollo server takes an object as its single argument. In our case, the object contains one item which is our GraphQL schema. The second endpoint is GraphiQL; an in-browser IDE for exploring GraphQL which we’ll be using to test our GraphQL server. Lastly, we start up a Node.js server.

That’s all we have to do to create our Node.js server.

Building The Schema

Let’s move on to create our schemas and their resolvers. Open src/schema/index.js and add the code below into it:

// src/schema/index.js

const { makeExecutableSchema } = require('graphql-tools');

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

const typeDefs = `

type Project {

id: Int!

name: String!

tasks: [Task]

}

type Task {

id: Int!

title: String!

project: Project

completed: Boolean!

}

type Query {

projectByName(name: String!): Project

fetchTasks: [Task]

getTask(id: Int!): Task

}

type Mutation {

markAsCompleted(taskID: Int!): Task

}

`;

module.exports = makeExecutableSchema({ typeDefs, resolvers });

First, we reference graphql-tools which will be used to build our schema and reference our resolvers (which we’ll create shortly). We define the schema for our app, we have Project and Task types. A project is something or a goal we’re trying to accomplish, while a task is a small thing which when done will help us to accomplish or goal or project. A project has three fields: id, name and tasks. Both the id and title fields are required fields. The tasks field is a collection of tasks from the Task type. A task has four fields: id, title, project and completed. Again, both the id and title fields are required fields. The project field signifies the Project a task belongs to. The completed field which can be either true or false, indicating whether a task is completed or not. Basically, a project can have many tasks and each task must belong to a project.

Next, we define some queries we’d like to run. projectByName takes the name of a project as an argument, gets a project by the name supplied and returns a single project. fetchTasks fetches all the tasks created and returns a collection of tasks, and getTasks takes the id of a task as an argument, gets a task by the id supplied and returns a single task.

We also define a mutation markAsCompleted which we’ll use to change the status of a task and hence mark the task as completed. It takes the id of the task as an argument and it will return the task after the update has been made. Finally, we use makeExecutableSchema to build our schema, passing to it our schema and the resolvers.

Writing Resolvers

Now we define our resolvers. A resolver is a function that defines how a field in a schema is executed.

Tip: GraphQL resolvers can also return Promises.

Open src/schema/resolvers.js and add the code below into it:

// src/schema/resolvers.js

const _ = require('lodash');

// Sample data

const { projects, tasks } = require('./../data/data');

const resolvers = {

Query: {

// Get a project by name

projectByName: (root, { name }) => _.find(projects, { name: name }),

// Fetch all tasks

fetchTasks: () => tasks,

// Get a task by ID

getTask: (root, { id }) => _.find(tasks, { id: id }),

},

Mutation: {

// Mark a task as completed

markAsCompleted: (root, { taskID }) => {

const task = _.find(tasks, { id: taskID });

// Throw error if the task doesn't exist

if (!task) {

throw new Error(Couldn't find the task with id ${taskID});

}

// Throw error if task is already completed

if (task.completed === true) {

throw new Error(Task with id ${taskID} is already completed);

}

task.completed = true;

return task;

}

},

Project: {

tasks: (project) => _.filter(tasks, { projectID: project.id })

},

Task: {

project: (task) => _.find(projects, { id: task.projectID })

}

};

module.exports = resolvers;

We reference both lodash and our sample data. Using lodash’s find(), we define functions to execute our queries: projectByName and getTask respectively. For fetchTasks, we simply return the array of tasks. For the markAsCompleted mutation, we first get the task by its id, then throw an error if the task doesn’t exist. We also throw an appropriate error if the task is already completed. If the task exists and is not already completed, we set it as true (that is completed) and return the task.

Since we have tasks and project as fields in our schema, we need to also define functions to execute them respectively. To get the tasks of a project, we use lodash’s filter(), which filters the tasks array and returns a new array containing only the tasks where their projectID equals the supplied project id. Lastly, to get the project a task belongs to, we use lodash’s find() which will return a project with the supplied task projectID.

Add Sample Data

We are almost done with our GraphQL server, we just need to add some sample data to test with. So, open src/data/data.js and add code below into it:

// src/data/data.js

const projects = [

{ id: 1, name: 'Learn React Native' },

{ id: 2, name: 'Workout' },

];

const tasks = [

{ id: 1, title: 'Install Node', completed: true, projectID: 1 },

{ id: 2, title: 'Install React Native CLI:', completed: false, projectID: 1 },

{ id: 3, title: 'Install Xcode', completed: false, projectID: 1 },

{ id: 4, title: 'Morning Jog', completed: true, projectID: 2 },

{ id: 5, title: 'Visit the gym', completed: false, projectID: 2 }

];

module.exports = { projects, tasks };

It’s now time to test our GraphQL server, using GraphiQL. Start up our Express server by executing the command below:

node src/server.js

The server should be running now and GraphiQL can be accessed through http://localhost:3000/graphiql. Go on and try it out with the query below:

{

projectByName(name: "Learn React Native") {

id

name

tasks {

title

completed

}

}

}

You should get a response like the demo below:

image.gif

Conclusion

We have seen what GraphQL is, the features it introduces and explained some of its concepts. We’ve also put all those into practice by building a simple GraphQL server. Obviously, there are some things I left out as this post is meant to be an overview of GraphQL. To learn more about GraphQL, check out the official documentation and also do check out the GraphQL specifications. You can also drop your questions and comments below.