by Oleg Ilyenko / @easyangel

Typical Rest API

Common Issues

  • Over-fetching
    • /products?field=name&field=description&field=variants[*].price
  • Under-fetching
    • /products?expand=productType&expand=variants[*].price.taxRate
  • API changes and evolution
    • Versioning
    • Deprecation
    • Maintenance

API Gateways

GraphQL Approach

GraphQL

  • A data query language
  • Developed by Facebook
  • Used internally since 2012
  • Open source version published in July 2015
  • Relay released in August 2015
  • Specification: https://facebook.github.io/graphql

Response Structure


query MyProduct {
  product(id: 123) {
    name
    description

    picture {
      width
      height
      url
    }
  }
}
          

{
  "data": {
    "product": {
      "name": "Delicious Cake",
      "description": "Just taste it!"

      "picture": {
        "width": 150,
        "height": 150,
        "url": "http://..."
      }
    }
  }
}
          

Every field Is a Function


query MyProduct {
  products {
    picture(size: 300) {
      width, height, url
    }
  }
}
          

{
  "data": {
    "products": [
      {
        "picture": {
          "width": 300,
          "height": 300,
          "url": "http://..."
        }
      },
      ...
    ]
  }
}
          

Aliases


query MyProduct {
  products {
    thumb: picture(size: 100) {
      width
    }
    fullSize: picture(size: 500) {
      width
    }
  }
}
          

{
  "data": {
    "products": [
      {
        "thumb": {
          "width": 100
        },
        "fullSize": {
          "width": 500
        }
      },
      ...
    ]
  }
}
          

Type System


type Picture {
  width: Int!
  height: Int!
  url: String
}
          

type Query {
  product(id: Int!): Product
  products: [Product]
}
          

interface Identifiable {
  id: String!
}
          

type Product implements Identifiable {
  id: String!
  name: String!
  description: String
  picture(size: Int): Picture
}
          

Mutations & Subscriptions


mutation ChangeStaff {
  changeName(productId: 123, newName: "Cheesecake") {
    id, version
  }

  setDescription(productId: 123, description: "Delicious!") {
    id, version
  }
}

subscription ProductEvents {
  nameChanged(productId: 123) { name }
  productDeleted { id }
}
          

Demo Time


{
  hero {
    id
    name
  }
}

// Response
{
  "data": {
    "hero": {
      "id": "2001",
      "name": "R2-D2"
    }
  }
}
          

Backend Agnostic

  • A Scala GraphQL implementation
  • Open source with Apache 2.0 licence
  • Conforms to GraphQL spec
  • Simple case-class based schema definition
  • Query parsing, validation and execution
  • Out-or-the-box support for different JSON ASTs:
    • play-json, spray-json, json4s, etc.

Schema Definition


import sangria.schema._

case class Product(
  name: String,
  description: Option[String])

val ProductType = ObjectType(
  "Product",
  "Commodity available for purchase",
  fields[Unit, Product](
    Field("name", StringType,
      resolve = _.value.name),
    Field("description", OptionType(StringType),
      resolve = _.value.description)
  )
)
          

Schema Definition


val QueryType = ObjectType("Query",
  fields[ProductRepo, Unit](
    Field("product", OptionType(ProductType),
      arguments = Argument("id", IntType) :: Nil,
      resolve = c =>
        c.ctx.getProductById(c.args.arg[Int]("id")))
  )
)

val AppSchema = Schema(QueryType)
          

Query Execution


import sangria.execution.Executor
import sangria.macros._

import sangria.marshalling.playJson._

val query = graphql"""
  query MyProduct {
    product(id: 123) {
      name
      description
    }
  }
  """

val result: Future[JsValue] =
  Executor.execute(AppSchema, query, userContext = new ProductRepo)
          

Play Example


class Application extends Controller {

  def graphql = Action.async(parse.json) { request =>
    val query = (request.body \ "query").as[String]

    val operation = (request.body \ "operation").asOpt[String]

    val variables = (request.body \ "variables").toOption.flatMap {
      case obj: JsObject => Some(obj)
      case _ => None
    }

    executeQuery(query, variables, operation)
  }

}
          

Play Example


def executeQuery(
        query: String,
        variables: Option[JsObject],
        operation: Option[String]) = {
  QueryParser.parse(query) match {

    case Success(parsedQuery) =>
      Executor.execute(AppSchema, parsedQuery,
        userContext = new ProductRepo,
        operationName = operation,
        variables = variables getOrElse Json.obj()) map (Ok(_))

    case Failure(error: SyntaxError) =>
      // can't parse GraphQL query, return error response
  }
}
          
Sangria Playground: http://try.sangria-graphql.org

Additional Features

  • Deferred values
    • Helps to solve N+1 query problem
  • Projections
    • Helps to build efficient DB projections
  • Middleware
    • Query execution instrumentation
    • Security primitives
    • Performance measurements
  • Query reducers
    • Query analysis before execution
    • Query complexity measurement

Thank you!




Questions?