Elixir+Absinthe

Cart00nHero8
5 min readAug 23, 2020

An intoxicating GRAPHQL

What is GRAPHQL?

A query language for your API
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

There are descriptions from the official website. Actually GraphQL is the real leading role in this article. Someone might ask we’ve already own RESTful, why should we use GraphQL? With the rapid growth of the hardware, client sides’s performance are faster and faster. So more and more business logic migrate to client that let client side become complex. In today software engineering, RESTful’s regular interface way seems not flexible enough right now. For an example, A view display a data in database. Some states only display some specific fields and some have to display all. In RESTful may needs multiple interfaces and client also needs to send multiple request to finish the business logic. It may waste resources and hard to let feature extend. But right now GraphQL only provide one interface and let client decide what data it wants. This solution make Api simple and scalable.

Elixir? Absinthe?

Elixir is a web DSL (Domain Specific Language). It’s father and mother are Erlang and Ruby. This language provide best distributed design. WhatsApp、Discord and Nintendo switch NPNS(NPNS handle 20 billion messages a day) are used elixir. And it’s architecture and “Let it crash” theory influence me deeply. Absinthe is the GraphQL Web framework of elixir. Absinthe let everything easy.

Let us begin

I’ll focus on introducing how to build GraphQL Api, so I’ll skip all knowledges about elixir. Again, GraphQL is the real leading role.

Step 1: install elixir and phoenix
Step 2: install libraries

defp deps do
[
{:absinthe, "~> 1.4"},
{:absinthe_plug, "~> 1.4"},
{:absinthe_phoenix, "~> 1.4"},
{:new_relic_absinthe, "~> 0.0.3"},
{:dataloader, "~> 1.0"}
]
end

Step 3: Start to build GraphQL API

There are three important members in GraphQL

Schema: GraphQL server uses a schema to describe the shape of your data graph. This schema defines a hierarchy of types with fields that are populated from your back-end data stores. The schema also specifies exactly which queries and mutations are available for clients to execute against your data graph.

Types: A GraphQL object type has a name and fields, but at some point those fields have to resolve to some concrete data. That’s where the scalar types come in: they represent the leaves of the query.

Resolver: A resolver is a function that’s responsible for populating the data for a single field in your schema. It can populate that data in any way you define, such as by fetching data from a back-end database or a third-party API.

OK,I know it’s convenience to write everything in schema, but I like form a good habit in the beginning.

Build your types files like this:

defmodule Schema.Type.AccountTypes do  use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
@doc "Foodie type"
object :foodie do
field :email, :string
field :name, :string
field :shop_branches, list_of(:shop_branch),
resolve: dataloader(Data)
end

After this I’ll introduce three actions in GraphQL:

Query: To query data from database

object :account_queries do
field :list_foodies, list_of(:foodie) do
resolve(&Resolvers.AccountResolver.list_foodies/3)
end

field :find_foodie, :foodie do
arg(:unique_id, non_null(:string))
resolve(&Resolvers.AccountResolver.find_foodie/3)
end
end

Mutation: Update data in database:

object :account_mutations do  field :create_foodie, :foodie do
arg(:email, non_null(:string))
arg(:name, non_null(:string))
resolve(&Resolvers.AccountResolver.create_foodie/3)
end
end

Subscribe: Trigger another action after an action was executed.
Ex: Update view after updating data

OK, in the sample code, you can see where to use resolver. Let us see how to implement it.

defmodule Resolvers.AccountResolver do  def list_foodies(_parent, _args, _resolutions) do
{:ok, Accounts.list_foodies()}
end

def find_foodie(_parent, args, _resolutions) do
{:ok, Accounts.find_foodie!(args[:unique_id])}
end

def create_foodie(_parent,%{} = args,_resolutions) do
new_args
|> Accounts.create_foodie
|> case do
{:ok, foodie} ->
{:ok, foodie}
{:error, changeset} ->
{:error, extract_error_msg(changeset)}
end
end
end

Nothing special, just how you handle database in the usual.

Context

In GraphQL, a context is an object shared by all the resolvers of a specific execution. It’s useful for keeping data such as authentication info, the current user, database connection, data sources and other things you need for running your business logic.

DataLoader

N+1 Problem:

Before introduce DataLoader, I have to tell about the performance issue in GraphQL: N+1 problem. Simply put, N + 1 is the number of db operations. For example:
Shops and Branches are has_many relationship. So when querying shops and their branches n+1 way will be:
Step1: query shops
Steps2: use shop_ids to search branches one by one.
If you are a backend master, you’ll discover step2 is where the issue is. That’s why Facebook develop DataLoader to fix it. So let us see how it works?

How DataLoader works:

There are two parts in DataLoader, Batching and Caching.
Batching: Gathering ids and when the times come, it search in DB once.
Caching: filter duplicated ids and reduce the number of searches.
In Absinthe, it provides an awesome tools let the codes are very simple:

def data() do
Dataloader.Ecto.new(PrjName.Repo, query: &query/2)
end
def query(queryable, params) do
case Map.get(params, :order_by) do
nil -> queryable
order_by -> from record in queryable, order_by: ^order_by
end
end

That’s why I said Absinthe let Graphql intoxicating. Fast and easy.

Finally in schema code:

defmodule Schema do
use Absinthe.Schema
import_types(Absinthe.Type.Custom)
import_types(Schema.Type.AccountTypes)
import_types(Schema.Type.InputTypes)
query do
import_fields(:account_queries)
import_fields(:gourmet_queries)
import_fields(:location_queries)
end
mutation do
import_fields(:account_mutations)
import_fields(:gourmet_mutations)
import_fields(:location_mutations)
end

def context(ctx) do
loader = Dataloader.new()
|> Dataloader.add_source(Data, Data.data())
Map.put(ctx, :loader, loader)
end
def plugins do
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
end
def middleware(middleware, _field, _object) do
[NewRelic.Absinthe.Middleware] ++ middleware
end
end

And then set up Router:

# GraphQL scope
scope "/api" do
pipe_through :api
forward "/gql_api", Absinthe.Plug,
schema: ProjectName.Schema.Schema
forward "/graphiql", Absinthe.Plug.GraphiQL,
schema: ProjectName.Schema.Schema,
interface: :simple
end

Demo:

References:
Graphql official webside:
https://graphql.org
Elixir/Phoenix official webside:
https://www.phoenixframework.org

Special thanks:
Elixir Taiwan
https://www.facebook.com/groups/elixir.tw

--

--