GraphQL for REST Devs

What is GraphQL?

At a high level, GraphQL is "a query language for your API"
More specifically, GraphQL is a specification for client <-> server interactions

What does GraphQL do?

  • A server defines a schema (rather than endpoints). Schemas are living documents, and are easy to change without breaking existing client code [1][2]
  • A client defines the data it expects, reducing the number of requests, and decreasing network usage. [3]
  • A gateway can use federation to compose sub-schemas from microservices into a super-schema, letting clients seamlessly query across services

[1]: Apollo Server was the library I used as a reference for capabilities, it is used by many Javascript libraries under the hood like TypeGraphQL and NestJS

[2]: The @deprecated declaration can be used to "soft remove" fields, adding new fields can be done without impacting existing queries, because they won't be resolved by that query

[3]: Apollo Client and urql were the libraries used for reference; however, libraries exist for many languages.

Example REST Api


GET /recipe/{id}
{
  id: number,
  title: string,
  description: string,
  ingredients: number[]
}
					

GET /ingredient/{id}
{
    id: number,
    title: string,
    description: string
}
				

Fetching Data (with REST)


async function getData(recipeId: number) {
	// Request #1
	const recipe = await fetch(`/recipes/${recipeId}`)
		.then(r => r.json());
	
	const ingredients = await Promise.all(
		// Request n?
		recipe.map(iId => fetch(`/ingredient/${iId}`)
			.then(i => i.json()))
	)

	return {
		...recipe,
		ingredients
	}
}
					

Fetching Data (with REST)


async function getData(recipeId: number) {
	// Request #1
	const recipe = await fetch(`/recipes/${recipeId}`)
		.then(r => r.json());
	
	const ingredients = await Promise.all(
		// Request n?
		recipe.map(iId => fetch(`/ingredient/${iId}`)
			.then(i => i.json()))
	)

	return {
		...recipe,
		ingredients
	}
}
					

Anatomy of a GraphQL Schema

  • ObjectTypes (output) and InputTypes (input) are declared, using either a Code First or Schema First paradigm
  • Resolvers are created, these are similar to Controllers, and define fields on each of the base types (Query, Mutation, Subscription)

The Apollo Server Docs have a great section on schemas that goes more in depth than this talk

GraphQL operations

  • Query
    Synonym for HTTP GET, Query operations should not change data
  • Mutation
    Similar to PUT/PATCH/POST, Mutation operations can update or create data
  • Subscription
    WebSockets under the hood, Subscription operations let clients opt-in to events from the server (i.e. chat messages, notifications)

An example GraphQL Schema


type Query {
  recipe(id: Int!): Recipe
  recipes: [Recipe!]!
}
type Mutation {
  updateRecipe(data: RecipeInput!, id: Int!): Recipe!
}
type Recipe {
  id: Float!
  title: String!
  description: String
  ingredients: [Ingredient]!
}					  
input RecipeInput {
  description: String
  title: String
}
				
The server exposes this as a standardized set of capabilities

An example GraphQL Schema


type Query {
  recipe(id: Int!): Recipe
  recipes: [Recipe!]!
}
type Mutation {
  updateRecipe(data: RecipeInput!, id: Int!): Recipe!
}
type Recipe {
  id: Float!
  title: String!
  description: String
  ingredients: [Ingredient]!
}					  
input RecipeInput {
  description: String
  title: String
}
				
Our operations are declared under the Query and Mutation types (Subscriptions would live in the Subscription type)

An example GraphQL Schema


type Query {
  recipe(id: Int!): Recipe
  recipes: [Recipe!]!
}
type Mutation {
  updateRecipe(data: RecipeInput!, id: Int!): Recipe!
}
type Recipe {
  id: Float!
  title: String!
  description: String
  ingredients: [Ingredient]!
}					  
input RecipeInput {
  description: String
  title: String
}
				
The Recipe model is declared here with the keyword type

An example GraphQL Schema


type Query {
  recipe(id: Int!): Recipe
  recipes: [Recipe!]!
}
type Mutation {
  updateRecipe(data: RecipeInput!, id: Int!): Recipe!
}
type Recipe {
  id: Float!
  title: String!
  description: String
  ingredients: [Ingredient]!
}					  
input RecipeInput {
  description: String
  title: String
}
				
The Recipe Input is declared here with the input type - this is a model that the server consumes

Fetching Data (with GQL)


async function getData(recipeId: number) {
  const query = gql`
    query recipeView($id: Float!) {
      recipe(id: $id) {
        title
        description
        ingredients {
          title
          description
        }
     }
  }`
	// Request #1
  return await gqlClient
                .query(query, {id: recipeId})
                .toPromise()
}
					

Fetching Data (with GQL)


async function getData(recipeId: number) {
  const query = gql`
    query recipeView($id: Float!) {
      recipe(id: $id) {
        title
        description
        ingredients {
          title
          description
        }
     }
  }`
	// Request #1
  return await gqlClient
                .query(query, {id: recipeId})
                .toPromise()
}
					

Questions, Comments, Concerns?

Demo time!

GraphiQL Playground

Client Demo

Repository