GraphQL tutorial: How to get started

GraphQL tutorial: How to get started

Tips and best practices for building your first service

·

11 min read

The pace of innovation is accelerating and business efficiency is becoming more critical than ever. To stay competitive, businesses need continuous access to data through connected applications.

Application Programming Interfaces (APIs) serve as the digital glue, connecting applications together and breaking down data silos. With APIs in place, developers can easily build new application features without having to build them from scratch.

GraphQL is a query language for interacting with APIs. It has grown in popularity in recent years as a more flexible and performant alternative to traditional REST APIs.

This guide will outline how to get started with GraphQL for beginners. With immersive code examples, we will explain how to set up and get started in GraphQL, as well as tips and best practices for building your first service.

What is GraphQL and how does It work?

GraphQL has its roots at Facebook. In the mid-2010s, Facebook was searching for a data-fetching API that would be powerful enough to "describe all of Facebook," while at the same time simple enough for developers to easily work with, especially when building microservices-based apps.

Faced with internal requirements, Facebook developed the Graph Query Language (or GraphQL) with a few key features:

  1. GraphQL was designed as an abstraction over an API, accessible using a single endpoint. This made it language-agnostic and easy to use since the underlying API could be built using any preferred language.
  2. GraphQL is accessible over a single endpoint using a composable query syntax. In comparison to REST, this design hides all underlying complexities and ensures you only get the data you need in a single round-trip without any underfetching or overfetching. Further, there is no need to deal with API versioning, so aging fields can be removed without affecting existing queries.

Getting started with your first GraphQL API

In this section, we will walk through a simplified version of the steps needed to create a GraphQL API.

Prerequisites

To follow along with this tutorial, make sure you have the following setup on your environment:

  1. Node.js version >= 16.0.0
  2. Fauna’s GraphQL

You can install Node.js by following the instructions at the link here. You can also register for a Fauna account here.

Initializing your project

Next, create a directory to place the GraphQL application and initialize your node project by running the following commands:

> mkdir fauna-graphql
> cd fauna-graphql
> npm init -y

Once these steps are executed, you should see a package.json file in the created directory.

Next, install the dependencies required for GraphQL as well as your server that will provide the GraphQL endpoint. In this example, we will be using Apollo Server, but you can also use other GraphQL servers such as Graphpack.

We will also be using the Fauna Javascript driver to interact with our database. Install the dependencies by running the following commands:

> npm install --save apollo-server graphql faunadb
> npm install -D nodemon

In your package.json file, update the following:

"scripts": {
   "serve": "nodemon index.js"
 },
"type": "module",

Next create the index.js file, which we will use as the entry point for our GraphQL APIs, by running:

> touch index.js

Create GraphQL schema

Next, we will create our schema that defines the structure of the data we will want to interact with. For this example, we will use a simple todo list, with each item containing the title for that todo item and a boolean flag to mark it as complete or incomplete.

In your index.js file, import the gql class and define the schema by adding the following. We will also define a query that will return a list of 0 or more todo items from our database.

import { ApolloServer, gql } from 'apollo-server-express';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import express from 'express';
import http from 'http';

const typeDefs = gql`
   type Todo {
       title: String
       completed: Boolean
   }

   type Query {
       allTodos: [Todo]
       todosByCompletedFlag(completed: Boolean): [Todo]
    }
;

Writing resolvers

Next, we will define our resolvers. A resolver is a collection of functions that generate responses for GraphQL queries. While we can think of schema as defining the structure of our API, resolvers define where and how data is interacted with.

First, we will use static data that we define in our index.js file to test our queries. Add the resolvers and static data using the following:

const resolvers = {
   Query: {
     allTodos: () => todos,
     todosByCompletedFlag(parent, args, context, info) {
        return todos.filter(todo => todo.completed === args.completed)
     }
   },
 };
 const todos = [
   {
     title: 'Install Dependencies',
     completed: false,
   },
   {
     title: 'Define Schemas',
     completed: true,
   },
   {
       title: 'Define Resolvers',
       completed: false
   }
 ];

Create your server instance and run it

Next, instantiate Apollo server by adding the following:

const app = express();
 const httpServer = http.createServer(app);

 const server = new ApolloServer({
   typeDefs,
   resolvers,
   plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
 });

 await server.start();
 server.applyMiddleware({
   app,
   path: '/',
 });

 // Modified server startup
 await new Promise(resolve => httpServer.listen({ port: 4000 }, resolve));
 console.log(`Server ready at http://localhost:4000${server.graphqlPath}`);

Start the server by running the following in your terminal:

> npm run serve

Interact with your data: Query to read or fetch values

You should now be able to access the GraphQL studio which lists out the queries just defined.

GraphQL Studio

Run the first query by adding the following in the middle section and hitting the Run button. This query reads all the Todo items, and fetches the title and completed status

query AllTodos {
 allTodos {
   title
   completed
 }
}

First query

The second query takes the ‘completed’ argument and returns the Todo items based on whether they are completed or not. Run the second query to return incomplete todo items by adding the following and hitting run.

query TodosByCompletedFlag {
 todosByCompletedFlag(completed: false) {
   title
   completed
 }

Run first query GraphQL

Now that we have verified that the server is working, we can update the resolvers to query Fauna instead of static data. Add the following to the index.js file to configure your connection settings.

const faunadb = require('faunadb');

const client = new faunadb.Client({
   secret: 'YOUR_FAUNA_SECRET',
   domain: 'db.us.fauna.com',
   // NOTE: Use the correct domain for your database's Region Group.
   port: 443,
   scheme: 'https',
 })

Interact with your data: Populate with sample data

Next add the following query to write sample Todo items to the Todo collection of your Fauna Database.

query  = faunadb.query;

 client.query(
   query.Map(
     [
       {
           title: 'Install Dependencies',
           completed: false,
       },
       {
           title: 'Define Schemas',
           completed: true,
       },
       {
         title: 'Define Resolvers',
         completed: false
       },
     ],
     query.Lambda(
       'todos',
       query.Create(
           query.Collection('todos'),
         { data: query.Var('todos') },
       )
     ),
   )
 )
 .then((ret) => console.log(ret))
 .catch((err) => console.error('Error: %s', err))

Log into Fauna and view the todos collection to see the Todo items inserted as documents.

todo documents

Next create an index on the todo title field by adding the following:

client.query(
   query.CreateIndex({
     name: 'todos_by_title',
     source: query.Collection('todos'),
     terms: [{ field: ['data', 'title'] }],
   })
 )
 .then((ret) => console.log(ret))
 .catch((err) => console.error('Error: %s', err))

Next, update the resolvers to query the database:

const resolvers = {
   Query: {
     allTodos() {
       return client.query(
           query.Map(
               query.Paginate(query.Match(query.Index('todos'))),
               query.Lambda(x => query.Get(x))
             )
         )
         .then((ret) => {
           var todos = [];
           ret.data.filter(todo => todos.push(todo.data));
           return todos;
       })
         .catch((err) => console.error('Error: %s', err))
     },
     todosByCompletedFlag(parent, args, context, info) {
       return client.query(
           query.Map(
               query.Paginate(query.Match(query.Index('todos'))),
               query.Lambda(x => query.Get(x))
             )
         )
         .then((ret) => {
           var todos = [];
           ret.data.filter(todo => {
               if(todo.data.completed === args.completed){
                   todos.push(todo.data)
               }
           });
           return todos;
       })
         .catch((err) => console.error('Error: %s', err))
     }
   },
 };

Rerun the previous queries using the GraphQL studio to verify that the data is coming from the database.

Todos by completed flag

Interact with your data: Add mutations to modify data

Next, let’s add a mutation to add new Todo items to our database. Define the mutation by adding the following to the GraphQL schema.

const typeDefs = gql`
   type Todo {
       title: String!
       completed: Boolean
   }

   type Query {
       allTodos: [Todo!]
       todosByCompletedFlag(completed: Boolean!): [Todo!]
    }

    type Mutation {
        addTodo(title: String!, completed:Boolean!): Todo!
    }
`;

Next, add the resolver that will write the todo item to the database and return the todo item once inserted.

const resolvers = {
   Query: {
     allTodos() {
       return client.query(
           query.Map(
               query.Paginate(query.Match(query.Index('todos'))),
               query.Lambda(x => query.Get(x))
             )
         )
         .then((ret) => {
           var todos = [];
           ret.data.filter(todo => todos.push(todo.data));
           return todos;
       })
         .catch((err) => console.error('Error: %s', err))
     },
     todosByCompletedFlag(parent, args, context, info) {
       return client.query(
           query.Map(
               query.Paginate(query.Match(query.Index('todos'))),
               query.Lambda(x => query.Get(x))
             )
         )
         .then((ret) => {
           var todos = [];
           ret.data.filter(todo => {
               if(todo.data.completed === args.completed){
                   todos.push(todo.data)
               }
           });
           return todos;
       })
         .catch((err) => console.error('Error: %s', err))
     }
   },
   Mutation: {
       addTodo(parent, args, context, info) {
           return client.query(
               query.Create(
                 query.Collection('todos'),
                 { data: {
                     title: args.title,
                     completed: args.completed
                   }
                },
               )
             )
             .then((ret) => {return ret.data})
             .catch((err) => console.error('Error: %s', err))
       }
   }
 };

From the GraphQL studio, run the following mutation.

mutation AddTodo {
 addTodo(title: "Add Mutations", completed: false) {
   title,
   completed
 }
}

Mutation addtodo

You can now see the todo item added to your database collection.

Database collection

Next we will add two more mutations,: one to update an existing todo items status, and one to delete the item. Update the schema definition by adding the following:

const typeDefs = gql`
   type Todo {
       title: String!
       completed: Boolean
   }

   type Query {
       allTodos: [Todo!]
       todosByCompletedFlag(completed: Boolean!): [Todo!]
    }

    type Mutation {
        addTodo(title: String!, completed:Boolean!): Todo!
        updateTodo(title: String!, completed:Boolean!): Todo!
        deleteTodo(title: String!): Todo!
    }
`;

Define the mutations by adding the following:

const resolvers = {
   Query: {
     allTodos() {
       return client.query(
           query.Map(
               query.Paginate(query.Match(query.Index('todos'))),
               query.Lambda(x => query.Get(x))
             )
         )
         .then((ret) => {
           var todos = [];
           ret.data.filter(todo => todos.push(todo.data));
           return todos;
       })
         .catch((err) => console.error('Error: %s', err))
     },
     todosByCompletedFlag(parent, args, context, info) {
       return client.query(
           query.Map(
               query.Paginate(query.Match(query.Index('todos'))),
               query.Lambda(x => query.Get(x))
             )
         )
         .then((ret) => {
           var todos = [];
           ret.data.filter(todo => {
               if(todo.data.completed === args.completed){
                   todos.push(todo.data)
               }
           });
           return todos;
       })
         .catch((err) => console.error('Error: %s', err))
     }
   },
   Mutation: {
       addTodo(parent, args, context, info) {
           return client.query(
               query.Create(
                 query.Collection('todos'),
                 { data: {
                     title: args.title,
                     completed: args.completed
                   }
                },
               )
             )
             .then((ret) => {return ret.data})
             .catch((err) => console.error('Error: %s', err))
       },
       updateTodo(parent, args, context, info){
           return client.query(
             query.Update(
               query.Select(['ref'], query.Get(query.Match(
                   query.Index("todos_by_title"), args.title
               ))),
               { data: { title: args.title, completed: args.completed } }
             ))
             .then((ret) => {return ret.data})
             .catch((err) => console.error('Error: %s', err))
       },
       deleteTodo(parent, args, context, info){
           return client.query(
               query.Delete(
                 query.Select(['ref'], query.Get(query.Match(
                     query.Index("todos_by_title"), args.title
                 )))
               ))
               .then((ret) => {return ret.data})
               .catch((err) => console.error('Error: %s', err))
        }
   }
 };

Run the following query to update the todo item.

mutation UpdateTodo {
 updateTodoStatus(title: "Add Mutations", completed: true) {
   title,
   completed
 }
}

Query to update the todo item

Verify that the document has been updated in the database.

Database collection 2

Run the following query to delete the todo item.

mutation DeleteTodo {
 deleteTodo(title:"Add Mutations") {
   title,
   completed
 }
}

Mutation delete todo

Verify that the document has been deleted in the database.

Database collection 3

Interact with your data: Setting up subscriptions

Subscriptions are used to establish a real-time connection with the server. Whenever an event occurs in the server and whenever that event is triggered, the server will send the corresponding data to the client.

Next we will set up a subscription that publishes a new Todo when it is added with the AddTodo mutation. First install the packages required by running the following:

> npm install subscriptions-transport-ws @graphql-tools/schema

Include the required packages by adding the following import statements

import { ApolloServer, gql } from 'apollo-server-express';
import express from 'express';
import { PubSub } from 'graphql-subscriptions';
import { createServer } from 'http';
import { execute, subscribe } from 'graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import faunadb from 'faunadb';

const pubsub = new PubSub();
const NEW_TODO = "NEW_TODO";

Update the addTodo mutation to publish newly added Todo items:

addTodo(parent, args, context, info) {
           return client.query(
               query.Create(
                 query.Collection('todos'),
                 { data: {
                     title: args.title,
                     completed: args.completed
                   }
                },
               )
             )
             .then((ret) => {
               pubsub.publish(NEW_TODO, { newTodo: ret.data }); 
               return ret.data})
             .catch((err) => console.error('Error: %s', err))
       }

Next, add the update the resolvers to include the subscription:

Subscription: {
       todoAdded: {
           subscribe:

             () => pubsub.asyncIterator([NEW_TODO]),
           resolve: (payload) => ({
             title: payload.newTodo.title,
             completed: payload.newTodo.completed
           })
       }
   }

Update the server config by replacing it with:

const app = express();
 const httpServer = createServer(app);

 const schema = makeExecutableSchema({
   typeDefs,
   resolvers,
 });

 const server = new ApolloServer({
   schema,
   plugins: [{
     async serverWillStart() {
       return {
         async drainServer() {
           subscriptionServer.close();
         }
       };
     }
   }],
 });

 const subscriptionServer = SubscriptionServer.create(
   { schema, execute, subscribe },
   { server: httpServer, path: server.graphqlPath }
 );

  await server.start();
 server.applyMiddleware({ app });

 const PORT = 4000;
 httpServer.listen(PORT, () =>
   console.log(`Server is now running on http://localhost:${PORT}/graphql`)
 );

Save the file and reload the Sandbox Studio page. Run the Subscription to start listening for AddTodo mutations by running the following:

subscription Subscription {
 todoAdded {
   title,
   completed
 }
}

The Subscriptions tab should open and listening

Subscription

In a new query tab, run another AddTodo mutation like this:

mutation AddTodo {
 addTodo(title: "Test Subscriptions", completed: false) {
   title,
   completed
 }
}

The mutation should be executed as well as published to the subscription.

Publish to subscription

Keep learning

GraphQL is a new technology, and in this guide, we have only explored a few basics. Here are some additional links you might find useful as you explore GraphQL:

  1. How to GraphQL - https://www.howtographql.com/
  2. The GraphQL Language Spec - https://github.com/graphql/graphql-spec
  3. GraphQL.org - https://graphql.org/

Looking for a serverless database with Native GraphQL?

Fauna is a flexible, developer-friendly, transactional database delivered as a secure and scalable cloud API with native GraphQL. Never again worry about database provisioning, scaling, sharding, replication, or correctness. Fauna has an extensive hub of GraphQL documentation, ideal for GraphQL novices and experts alike.