A
GraphQL in Elixir
Primer
Bruce Williams
Bruce williams

Twitter: @wbruce (DMs open)
GitHub: @bruce
Elixir Forum: @bruce
Elixir Slack: Bruce Williams


industry 1
INDUSTRY 2
INDUSTRY 3
co-Creator

EST. Oct 2015
v1.4
Absinthe
co-author

beta now, print soon
Ben Wilson

tempus
FUGIT
agenda
- Basic background on GraphQL, how it works, some benefits, and how you might choose to employ it.
- Getting started with Absinthe, the GraphQL toolkit for Elixir, some handy glossary words, and answers to some frequently asked questions.
GraphQL
Basics
Graph
Query
Language
data query language
compare to: sql
query {
post(id: "abc-123") {
title
publishedDate
author {
name
}
body
}
}
{
"data": {
"post": {
"title": "CodeBEAM SF",
"publishedDate": "2018-03-19",
"author": {
"name": "Bruce Williams"
},
"body": "..."
}
}
}
you get what you ask for
http api
compare to: rest
query ($userId: ID!, $since: Date) {
user(id: $userId) {
posts(since: $since) {
title
publishDate
}
}
}
{"userId":"12", "since":"2018-01-01"}
{
"data": {
"user": {
"posts":[
{"title": "Example1", "publishDate":"2018-01-01"},
{"title": "Example2", "publishDate":"2018-01-02"}
]
}
}
}
GraphQL
variables
result
option #1
replace rest
Database
graphql api
option #2
aggregate services
rest API
graphql api
rest API
etc
option #3
abstract data access
database
REST api
graphql (directly)
option #?
mix and match
handy Elixir Adoption Idea
it's not just about queries
query
Subscribes to an event,
requests information be
sent later (over WS, etc)
query ($search: String, $limit: Int = 10) {
posts(matching: $search, limit: $limit) {
title
author { name }
}
}
mutation
Requests information.
mutation ($postContent: PostInput!) {
createPost(input: $postContent) {
assignedEditor { name }
publicationStatus
}
}
subscription ($postId: ID!) {
reviewCompleted(postId: $postId) {
editor { name }
requestedChanges {
changeType
notes
}
}
}
Modifies data, requests
information in response.
subscription
GraphQL Operations
websocket!
how it works
schema
a graph of relationships between types
Post
Comment
User
posts
author
comments
subject
String
Date
name
birthdate
Object types
, fields
, and extensible scalar types
... plus enumerations, unions, interfaces, and others
Defining a GraphQL Schema
Post
User
posts
Defining a GraphQL Schema
Resolver
w/ defined arguments
Post
User
posts
Query
Root
posts
post
user
users
Root object types are entry points for GraphQL operations
Defining a GraphQL Schema
graphql document
describes a path to walk through a schema graph
a graphQL query narrative
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
This is a query operation, resolve the root query value.
Resolve its posts field, given the provided matching and limit arguments. Expect a list of values we can treat as Post object typed as the result.
For each Post typed value...
a graphQL query narrative
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
Resolve its title field. Expect a value that we can handle as a String scalar type as the result. Save it.
Resolve the post's author field. Expect a value we can treat as a User object type as the result.
With that User value...
a graphQL query narrative
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
Resolve the name field. Expect a value that we can treat as a String scalar type as the result. Save it.
No more fields for the User typed value. Go back up a level to the last Post typed value.
Resolve its comments field, given the provided input values for the limit and order arguments. Expect a list of values we can treat as Comment typed as the result.
a graphQL query narrative
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
Resolve the body field. Expect a value we can treat as a String scalar type as the result. Save it.
No more fields for the Comment typed value, go back up a level to the last Post typed value.
For each Comment typed value...
No more fields for the Post typed value, go back up a level to the root query type value.
a graphQL query narrative
query {
posts(matching: "conference", limit: 10) {
title
author { name }
comments(limit: 3, order: {by: VOTES, direction: DESC}) {
body
}
}
}
No more fields for the root query type value, collect the saved data and return it as a tree data structure.
introspection
tools

USING ABSINTHE
installation
Standard installation; in your mix.exs:
defp deps do
[
# As expected...
{:absinthe, "~> 1.4.0"},
# If you want to use HTTP (likely!)
{:absinthe_plug, "~> 1.4.0"},
# Rest of deps...
]
end
define a schema
A module, wherever you want to put it:
defmodule MyAppWeb.Schema do
use Absinthe.Schema
# More on this later...
end
setup for the web minimalist
using with plug
setup with plug
If you only care about application/graphql requests:
plug Absinthe.Plug,
schema: MyAppWeb.Schema
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
json_decoder: Poison
plug Absinthe.Plug,
schema: MyAppWeb.Schema
This is the more standard approach, supporting application/json:
SETUP for the WEB pragmatist
using with phoenix
PHOENIX SETUP
If you only care about GraphQL requests, add to your Endpoint:
defmodule MyApp.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
plug Plug.RequestId
plug Plug.Logger
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
pass: ["*/*"],
json_decoder: Poison
plug Absinthe.Plug,
schema: MyAppWeb.Schema
end
... and remove your Router.
PHOENIX SETUP
If you only want GraphQL on a particular route, add to your Router:
defmodule MyAppWeb.Router do
use Phoenix.Router
resource "/pages", MyAppWeb.PagesController
forward "/api", Absinthe.Plug,
schema: MyAppWeb.Schema
end
standard subscriptions setup
Add to your mix.exs:
def deps do
[
# Others...
{:absinthe_phoenix, "~> 1.4.0"}
]
end
[
# Other children ...
supervisor(MyAppWeb.Endpoint, []),
supervisor(Absinthe.Subscription, [MyAppWeb.Endpoint]),
# Other children ...
]
Add this to your Application:
standard subscriptions setup
Add to your Endpoint:
use Absinthe.Phoenix.Endpoint
Add this to your socket, for example, UserSocket:
use Absinthe.Phoenix.Socket,
schema: MyAppWeb.Schema
Authentication/authorization concerns can be handled in connect/2.
standard subscriptions setup
For JavaScript clients, see:
https://github.com/absinthe-graphql/absinthe-socket
Standard JS client + drop-in support for the Relay and Apollo client-side GraphQL frameworks.
Thank you, Christian and Mauro!
define schemas
using elixir macros
Post
User
posts
author
defmodule MyAppWeb.Schema do
use Absinthe.Schema
# Rest of schema...
object :post do
field :title, :string
field :publish_date, :date
field :author, :user
field :body, :string
end
object :user do
field :name, :string
field :posts, list_of(:post)
end
end
object :user do
field :name, :string
field :posts, list_of(:post), resolve: &find_posts/3
end
def find_posts(user, _args, _info) do
posts = Repo.all(Ecto.assoc(user, :posts))
{:ok, posts}
end
Basic Resolver
standard graphql
VALIDATION
PARSING
EXECUTION
ABSINTHE
PHASES
40+
frequently
asked
questions
how do you handle authorization?
Q:
populate the absinthe context with the current user, then use/check that user. middleware helps!
A:
auth: writing the CONTEXT plug
defmodule MyAppWeb.Context do
@behaviour Plug
import Plug.Conn
def init(opts), do: opts
def call(conn, _) do
context = build_context(conn)
Absinthe.Plug.put_options(conn, context: context)
end
@doc """
Return the current user context based on the authorization header
"""
def build_context(conn) do
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
{:ok, current_user} <- authorize(token) do
%{current_user: current_user}
else
_ -> %{}
end
end
defp authorize(token) do
# Authorization logic...
end
end
AUTH: wiring in the CONTEXT plug
If you're just using Plug:
plug MyAppWeb.Context
plug Absinthe.Plug,
schema: MyAppWeb.Schema
pipeline :graphql do
plug MyAppWeb.Context
end
scope "/api" do
pipe_through :graphql
forward "/", Absinthe.Plug,
schema: MyAppWeb.Schema
end
If you're using Phoenix, then use a pipeline in your Router:
AUTH: using the current user
In a structural query:
query do
field :me, :user do
resolve fn
_, _, %{context: context} ->
{:ok, Map.get(context, :current_user)}
end
end
end
object :user do
@desc "Get the user's posts"
field :posts, list_of(:post), resolve: dataloader(Post)
end
query {
me {
posts {
title
}
}
}
AUTH: using the current user
In an ad hoc check:
query do
@desc "Get the full list of authors"
field :authors, list_of(:authors) do
resolve fn
_, _, %{context: %{current_user: %{admin: true}}} ->
{:ok, Repo.all(User)}
_, _, _ ->
{:error, "Unauthorized"}
end
end
end
query {
authors {
name
}
}
AUTH: using the current user
Writing middleware:
defmodule MyAppWeb.Middleware.AdminRequired do
@behaviour Absinthe.Middleware
def call(resolution, _) do
case resolution.context do
%{current_user: %{admin: true}} ->
resolution
_ ->
Absinthe.Resolution.put_result(
resolution,
{:error, "Unauthorized"}
)
end
end
end
end
AUTH: using the current user
Applying it ad hoc, per field:
field :protected_thing, :thing do
middleware MyAppWeb.Middleware.AdminRequired
resolve &resolve_thing/3
end
or, applying it using middleware/3:
def middleware(middleware, %{identifier: :protected_thing} = _field, _object) do
[MyAppWeb.Middleware.AdminRequired | middleware]
end
def middleware(middleware, _field, _object), do: middleware
how do you resolve fields concurrently?
Q:
Use the async plugin.
A:
field :time_consuming, :thing do
resolve fn _, _, _ ->
async(fn ->
{:ok, long_time_consuming_function()}
end)
end
end
async plugin
Uses Task.async under the hood (with a default timeout of 30s).
Absinthe.Middleware.Async also serves as an excellent example of how to build middleware.
how do you batch resolution to avoid N+1 query problems?
Q:
Use the batching plugin (or even better, dataloader).
A:
object :post do
field :name, :string
field :author, :user do
resolve fn post, _, _ ->
batch({__MODULE__, :users_by_id}, post.author_id, fn batch_results ->
{:ok, Map.get(batch_results, post.author_id)}
end)
end
end
end
def users_by_id(_, user_ids) do
users = Repo.all from u in User, where: u.id in ^user_ids
Map.new(users, fn user -> {user.id, user} end)
end
batching plugin
def deps do
[
{:dataloader, "~> 1.0.0"},
# ...
]
end
using dataloader
Add the dependency to your mix.exs:
defmodule MyApp.Blog
def data do
# Can customize how queries are built by
# passing a function
Dataloader.Ecto.new(MyApp.Repo)
end
end
Setup the dataloader source (usually in your Phoenix bounded context):
object :post do
field :name, :string
field :author, :user, resolve: dataloader(Blog)
end
using dataloader
Use it in your schema:
how do you break-up a schema module?
Q:
extract types to other modules, then import them.
A:
A bad schema structure can be exhausting.
defmodule MyAppWeb.Schema.PublishingTypes do
use Absinthe.Schema.Notation
object :post do
field :title, :string
field :publish_date, :date
field :author, :user
field :body, :string
end
object :post_queries do
field :posts, list_of(:posts)
# Other fields for root query type
end
# Other types...
end
extract types
defmodule MyAppWeb.Schema do
use Absinthe.Schema
import_types MyAppWeb.Schema.{
PublishingTypes,
AccountTypes
}
query do
import_fields :post_queries
import_fields :user_queries
end
end
import types & fields

v1.5
NEXT:
refactored schema definition system
new parser + pluggability
schema stitching
graphql SDL support
more flexible imports
elixir graphql client
dataloader improvements
thank you!
@wbruce
http://absinthe-graphql.org
https://pragprog.com/book/wwgraphql
20% off (until Sun):

CodeBEAM SF 2018
A GraphQL in Elixir Primer
By wbruce
A GraphQL in Elixir Primer
My talk for CodeBEAM SF 2018, covering the use of GraphQL in the Elixir programming language.
- 1,649