Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Designing a Functional GraphQL Library

340 views

Published on

GraphQL is becoming a strong alternative to REST for exposing APIs. In this talk, I will introduce Caliban, a new functional library for writing GraphQL backends in Scala. I will go through the process of creating the library from scratch and explain my design choices: how to minimize boilerplate, how to expose a pure API with explicit errors, how to handle subscriptions, etc.

Published in: Software
  • Be the first to comment

Designing a Functional GraphQL Library

  1. 1. DESIGNING A FUNCTIONAL GRAPHQL LIBRARY Functional Scala 2019
  2. 2. WHO AM I? > Pierre Ricadat aka @ghostdogpr > ! exiled in " > Contributor to ZIO > Creator of Caliban
  3. 3. GRAPHQL ?
  4. 4. GRAPHQL IN A NUTSHELL > query language for APIs > server exposes a typed schema > client asks for what they want > queries > mutations > subscriptions
  5. 5. WHY?
  6. 6. WHY CREATE A GRAPHQL LIBRARY? > Sangria is great but... > lots of boilerplate > Future-based > schema and resolver tied together > interesting approach by Morpheus (Haskell) > it's fun!
  7. 7. GOALS > minimal boilerplate > purely functional > strongly typed > explicit errors > ZIO > schema / resolver separation
  8. 8. PLAN OF ACTION query → parsing → validation → execution → result ↑ schema
  9. 9. QUERY PARSING query → parsing → validation → execution → result ↑ schema
  10. 10. QUERY PARSING GraphQL spec: https://graphql.github.io/graphql-spec/
  11. 11. QUERY PARSING Fastparse: fast, easy to use, good documentation def name[_: P]: P[String] = P(CharIn("_A-Za-z") ~~ CharIn("_0-9A-Za-z").repX).! def fragmentName[_: P]: P[String] = P(name).filter(_ != "on") def fragmentSpread[_: P]: P[FragmentSpread] = P("..." ~ fragmentName ~ directives).map { case (name, dirs) => FragmentSpread(name, dirs) }
  12. 12. SCHEMA query → parsing → validation → execution → result ↑ schema
  13. 13. SCHEMA Idea from Morpheus: derive GraphQL schema from basic types
  14. 14. SIMPLE API type Queries { pug: Pug! } case class Queries(pug: Pug)
  15. 15. OBJECTS type Pug { name: String! nicknames: [String!]! favoriteFood: String } case class Pug( name: String, nicknames: List[String], favoriteFood: Option[String])
  16. 16. ENUMS enum Color { FAWN BLACK OTHER } sealed trait Color case object FAWN extends Color case object BLACK extends Color case object OTHER extends Color
  17. 17. ARGUMENTS type Queries { pug(name: String!): Pug } case class PugName(name: String) case class Queries(pug: PugName => Option[Pug])
  18. 18. EFFECTS type Queries { pug(name: String!): Pug } case class PugName(name: String) case class Queries(pug: PugName => Task[Pug])
  19. 19. RESOLVER simple value case class PugName(name: String) case class Queries( pug: PugName => Task[Pug] pugs: Task[List[Pug]] ) val resolver = Queries( args => PugService.findPug(args.name), PugService.getAllPugs )
  20. 20. SCHEMA trait Schema[-R, T] { // describe the type T def toType(isInput: Boolean = false): __Type // describe how to resolve a value of type T def resolve(value: T): Step[R] }
  21. 21. WHAT'S A STEP > Pure Value (leaf) > List > Object > Effect > Stream
  22. 22. SCHEMA FOR STRING implicit val stringSchema = new Schema[Any, String] { def toType(isInput: Boolean = false): __Type = __Type(__TypeKind.SCALAR, Some("String")) def resolve(value: String): Step[R] = PureStep(StringValue(value)) }
  23. 23. Schema for case classes and sealed traits?
  24. 24. MAGNOLIA > ! > ease of use > compile time > documentation, examples > " > error messages (getting better!)
  25. 25. MAGNOLIA DERIVATION def combine[T]( caseClass: CaseClass[Typeclass, T]): Typeclass[T] def dispatch[T]( sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T]
  26. 26. VALIDATION query → parsing → validation → execution → result ↑ schema
  27. 27. VALIDATION Back to the specs
  28. 28. EXECUTION query → parsing → validation → execution → result ↑ schema
  29. 29. EXECUTION 1. Schema + Resolver => Execution Plan (tree of Steps) 2. Filter Plan to include only requested fields 3. Reduce Plan to pure values and effects 4. Run effects
  30. 30. N+1 PROBLEM { orders { # fetches orders (1 query) id customer { # fetches customer (n queries) name } } }
  31. 31. QUERY OPTIMIZATION Naive ZIO[R, E, A] version val getAllUserIds: ZIO[Any, Nothing, List[Int]] = ??? def getUserNameById(id: Int): ZIO[Any, Nothing, String] = ??? for { userIds <- getAllUserIds userNames <- ZIO.foreachPar(userIds)(getUserNameById) } yield userNames
  32. 32. QUERY OPTIMIZATION Meet ZQuery[R, E, A] val getAllUserIds: ZQuery[Any, Nothing, List[Int]] = ??? def getUserNameById(id: Int): ZQuery[Any, Nothing, String] = ??? for { userIds <- getAllUserIds userNames <- ZQuery.foreachPar(userIds)(getUserNameById) } yield userNames
  33. 33. ZQUERY BENEFITS > parallelize queries > cache identical queries (deduplication) > batch queries if batching function provided
  34. 34. ZQUERY > courtesy of @adamgfraser > based on paper on Haxl > "There is no Fork: An Abstraction for Efficient, Concurrent, and Concise Data Access" > will be extracted to its own library at some point
  35. 35. INTROSPECTION Dogfooding case class __Introspection( __schema: __Schema, __type: __TypeArgs => __Type)
  36. 36. USAGE 1. Define your queries / mutations / subscriptions 2. Provide a Schema for custom types 3. Provide a resolver 4. Profit
  37. 37. 1. DEFINE YOUR QUERIES case class Pug(name: String, nicknames: List[String], favoriteFood: Option[String]) case class PugName(name: NonEmptyString) case class Queries(pugs: Task[List[Pug]], pug: PugName => Task[Pug]) type Pug { name: String! nicknames: [String!]! favoriteFood: String } type Queries { pugs: [Pug!] pug(name: String!): Pug }
  38. 38. 2. PROVIDE A SCHEMA FOR CUSTOM TYPES implicit val nesSchema: Schema[NonEmptyString] = Schema.stringSchema.contramap(_.value)
  39. 39. 3. PROVIDE A RESOLVER def getPugs: Task[List[Pug]] = ??? def getPug(name: String): Task[Pug] = ??? val queries = Queries( getPugs, args => getPug(args.name)) val resolver = RootResolver(queries)
  40. 40. 4. PROFIT val interpreter = graphQL(resolver) for { result <- interpreter.execute(query) _ <- zio.console.putStrLn(result.toString) } yield ()
  41. 41. HTTP4S MODULE val route: HttpRoutes[RIO[R, *]] = Http4sAdapter.makeRestService(interpreter) val wsRoute: HttpRoutes[RIO[R, *]] = Http4sAdapter.makeWebSocketService(interpreter)
  42. 42. SCALAJS Few and modern dependencies ScalaJS support for free
  43. 43. CATS COMPATIBILITY caliban-cats module def executeAsync[F[_]: Async](...)( implicit runtime: Runtime[Any]): F[GraphQLResponse[E]] def makeRestServiceF[F[_]: Effect, Q, M, S, E](...)( implicit runtime: Runtime[Any]): HttpRoutes[F]
  44. 44. PERFORMANCE Caliban is fast Check benchmarks module on github
  45. 45. FUTURE > Query analysis > Middleware (query-level, field-level) > Code generation > Other HTTP adapters (Akka HTTP) <- help needed > Client support
  46. 46. WANNA KNOW MORE? > Website: https://ghostdogpr.github.io/caliban/ > ZIO Discord: #caliban
  47. 47. THANKS!QUESTIONS?

×