Really quick introduction to GraphQL
GraphQL is a query language for API’s. Some people name it successor of
REST, I’d rather say it supplementor of
REST because both can work together. In this article you’ll find good comparison of both.
In short: GraphQL is a query language for APIs, optimized for performance, designed and open-sourced by Facebook. In GraphQL you can ask server for connected data and you’ll get in response only what you’ve asked for. Not more. If you need more information about this, you’ll find it on GraphQL webpage
To setup GraphQL backend server we’ll use few libraries. The first is Akka HTTP – one of the most popular HTTP servers in the Scala world nowadays. The library responsible for GraphQL implementation is Sangria. We’ll also use Slick with an in-memory database.
The source code for starting point you can find here. Feel free to fork this repository.
master branch has final version of our code, but there are also branches ‘stage#’ representing states between. For easier tracking changes between stages I prepared patch files with differences. You can apply such patch (i.e.
git apply stage1-stage2.patch) and your code editor will highlight all changes that were made.
The branch mentioned above has code able to run the basic server. There are all dependencies and plugins configured. I assume you’re at least little familiar with the Scala world and I don’t need to explain where to find such information in the codebase :D.
Let’s look into the code now. The basic Akka HTTP server is defined in
As you can see,
route definition has only two endpoints. Every
/graphql endpoint is delegated to
GraphQLServer, everything else is managed by
graphiql.html file (placed in resources btw).
graphiql.html is a basic HTML console you can use to test your GraphQL backend. Here you can find an example to see how it should looks like. In my code I use slightly changed version of Graphiql provided by Sangria. When you run the server (
sbt run) you should be able to use this console now, it’s configured to connect to our server so you can use it for testing.
Models you can find entire domain we will be working with. In short: our domain is defined by Categories and Products related one to each other. Between them is a many-to-many relation, so in the file you can find simple case class representing this.
Models.scala file there are also things I have to explain now. Firstly, I’ve defined type aliases
ProductId. The reason for that is to improve the readability the code I’ll explain further.
Sangria library has not-the-best documentation, I would say. Especially when you’re working with relations it’s really easy to mess the types, to avoid this I decided to use type aliases.
There is also
Identifiable trait in file. In this case I wanted to highlight support for polymorphic types provided by Sangria. Similar is for
Picturemodel with factory method inside
Product – I wanted to show you, how to use such kind of structures. I hope the rest of the file should be understandable and there is no need to explain it.
As you can find in
application.conf the server uses in-memory database which is populated with data during start. All the things related to DB connection I placed in one file:
ShopRepository. There you can find Slick Schema model definitions. There are also accessors for either single records and collections. It gives us 6 functions at this moment.
In the repository there are only two other files worth mentioning:
SchemaDef. The first one is responsible for handling GraphQL calls and the second one is responsible for interpreting GraphQL queries. Both are described more detailed in the next section.
As we saw previously, all the
POST requests targeted to
/graphql endpoint server delegates to
GraphQLServer file. Let’s look onto this now. The server expects JSON model with the following shape:
It’d shed more light on topic when you’ve already saw the content of the file.
GraphQLServer tries to read these three root objects and pass it to the executor.
query is the GraphQL query itself,
variables is additional data for that query. In GraphQL you can send the query and arguments separately.
You can also set name for the query, it’s what the third object is for. Imagine that query is like a function, usually you’re using anonymous functions, but for logging or other purposes you could add names. It’s send as
operationName. More about that you can read here.
The most important call in this file is
If executor responds with success, the result is send back to the client, in other case server will responds with status code 4xx and some kind of explanation what was wrong with the query.
Executor also needs few things from
SchemaDef file, so let’s discuss them now.
SchemaDef is the most important file in our example project. It contains our Schema – what we are able to query for. It also interprets how data is fetched and from which data source (i.e. one or more databases, REST call to the other server…). In short our
SchemaDef file defines what we want to expose. There are defined types (from GraphQL point of view) and shape of the schema a client is able to query for.
Models file contains classes for our domain, now we have to expose it. But we cannot use the same case classes, we have to define types what are understandable by Sangria. It’s worth pointing out that there doesn’t have to be a 1-1 mapping between our Sangria and DB models. This abstraction allows us to freely hide, add or aggregate fields.
Let’s discuss it on this simple class:
Now we have to define proper
ObjectType for this case class. We have to map all the fields like this:
Let’s look onto it line by line:
ObjectType[Unit, Picture] tells us that it’s ObjectType for our
Picturecase class. But what is the
Unit there? It’s
Context type. In our example we’re using
Unit because we don’t need it at all, but in complex, production schemas probably you will. Context is an object that flows across the whole execution. Usually it doesn’t change state, but if you need you can store some helpful data there to use in following sub queries.
Often developers put there object responsible for the access to the data source but you can use it in other way. For example (especially if you use middleware) you can put there users permissions’ list for easy access in the next processing step. To be clear: Context lives only during one execution, I mean: from request to response.
Two next lines define
name and description for our type. Server also exposes documentation for schema, so don’t be shy and document your types!
Next we’re defining fields. Each
Field, has name, type and resolver. Typically you can use resolver as function that read the value from provided case class and context.
In our example above we’re mapping
height exactly like in case class, but for
url we’ve added also a description.
Let’s look at the produced schema. Run the project and open the browser at
http://localhost:8080, on top-right you have
Docs tab, open it now and search for
Picture type. You should see something like this:
Easy like this. But it could be even easier. Sangria provides some macros that read the case classes and creates proper object types for it. For example, you can define type for
Picture like this:
deriveObjectType is included in package
sangria.macros.derive so don’t forget to import this. Macros are handy for simple cases, but they can only extract data from case classes, not read your mind. So the rest you will have to add manually. For instance documentation: you have to put all the information manually.
The code above will produce exactly the same object type for picture like we made few lines above.
Let’s look on types defined in
SchemaDef once again. There you can find also definition of
Identifiable type we’ve used as parent for both:
To define such interface you should use
InterfaceType instead of
ObjectType and then you can pass it to proper object type. In the definition of
ProductType there is also
IncludeMethods("picture") what tells the macro which additional functions we want to expose in our schema.
Please look on the last two constants defined in the file.
QueryType is one more
ObjectType, but it defines our root schema, it defines what we allow to fetch for, actually… it’s used in the
Schema at the last line and passed to
QueryType has inside? There are three fields exposed:
For now we don’t touch categories.
allProducts returns list of products
ListType(ProductType) and use
ShopRepository (provided as context) and its function with the same name (
allProducts). Easy like this.
IntType and similar basic types are already defined by Sangria so you don’t have to do it yourself.
In the example above we’re also expecting arguments.
arguments property is always a list of
Argument hence you can see
:: Nil at the end.
Argument("id", IntType) means it expects argument of type
Integer and name
id, the same argument we are passing to the
ctx.product function in the resolver, so we can even extract it as variable instead of keep it inline.
It’s worth of extracting especially, when you want to reuse it in the in other types. You can replace the code above with the following.
That’s all for this step. You can run the server, open the console and play with that. Remember, at this step we have only three fields exposed.
There are example queries you can use:
You can also pass some values as variables, you can use such query in that case:
But then you have to provide values as JSON in the the
Query variablesinput, like this:
So far, so good. But what about performance? What if we execute a query like this?
As we remember,
products field fetches data directly from database. In this case it fetches three times same results. To optimize this, Sangria provides
Fetchers and Deferred Resolvers
Deferred Resolvers are mechanisms for batch retrieval of object from their sources like database or external API.
Deferred Resolverprovides low-level efficient API but, on the other hand, it’s more complicated in use and less secured.
Fetcher is a specialized version of
Deferred Resolver. It provides high-level API, it’s easier to use and usually provides all you need. It optimizes resolution of fetched entities based on its ID or relation, it deduplicates entities and caches the results, to name but a few possibilities.
Let’s implement one for
and change fields in
QueryType to use this:
We’re still using
repo.products to fetch products from database, but now it’s wrapped in
Fetcher. It optimizes the query before call. Firstly it gathers all data it should fetch and then it executes the query. Caching and deduplication mechanisms allow to avoid duplicated queries and give results faster.
Ok, if we have our fetcher defined, we push it to lower level.
Such resolver have to be passed into the
Executor to make it available for use. As you can see, we use the same fetcher in two fields, in the first example we’re providing only one ID and expecting one object (
defer function) in the second case we’re providing a list of IDs and expecting a sequence of objects (
deferSeq). Look at the Sangria docs if you’re interested in other cases.
Since, we’re using
ShopRepository.products to fetch single entity or entire list, we don’t need
product function anymore.
To extract ID from entities, Resolver uses
HasId type class. You have few choices how to provide such class for your model. Firstly you can explicitly pass it.
On the other hand, you can declare implicit constant in the same context so fetcher will take it implicitly. For example you can use generic HasId for every child of
Identifiable trait and import it to the context.
The third way is the way I’ve chosen. Providing
HasId implicitly inside Model, like this:
BTW you can read more about type classes in another good article on our blog.
Applying the last changes should makes your code similar to branch stage2. In repository there is also code responsible for Category entities, but we will skip it here – it’s pretty similar .
Implementing Many-to-Many relation
Time to implement relation between
Category models. One-to-Many is much easier to manage but I chose to make some extra miles.
Our plan is to make us able to execute such queries:
In short, we want to get all categories related to proper product, and in opposite – all products in category. To make this plan done we have to:
- Define relation between entities using
- extend already defined
Fetcherto find the entities based on that relation,
- add proper fields in exposed schema,
- finally we need to implement function which will fetch proper records from database.
Let’s make it happen.
Defining a relation between Product and Category will be the first thing to do, so let’s look how it works in Sangria
Relation costructor’s signature looks like that:
T in this case, is the type of model for which we’re defining relation,
RelId is type of Id of related model.
In example where we have categories stored in database as list of ids like:
We are able to define relation in this way:
Probably it’d fit big part of your needs, but not ours… We’ve added few extra miles and used model with many-to-many relation, so it isn’t impossible to do as in the example above. Of course we could define our schema definition and fetch all
Category’s ids by Slick, while records are loaded from database. Yes, we could. But it produces additional database operation even if we don’t need such data. But there’s another way.
There is another
Relation constructor we can use:
This constructor needs additional object (
Tmp) and uses this to retrieve a model and its relations.
In our example, we want to find all categories for product, so
T will be
Category(type of related model) and
RelId will be
ProductId(Id of model we’re looking relation for), so we can define our relation like this:
Note: Sangria uses
Seq[IdType] in every function related to ID’s, even if function logically should work for single ID only. In some cases it could be misleading.
We used tuple
(Seq[ProductId], Category) as temporary object, so it’s easy to retrieve proper data. Relation is defined, but where we’ll get such temporary object from? The most logical answer is: from data source. Yes, in our case database. We have to provide function, that gets product id as argument and return sequence of such tuples. Remember that even for single id, we use
So far, so good. We are in the middle of our list now. The next step is
Fetcher. As I mentioned previously, one of the features of this type class is support for model ID along with relation ids. Now we have to tell fetcher, there is relation defined and it can be used when needed. We have to refactor our
categoriesFetcher which now looks like this:
This basic constructor uses
apply function, but to add support for relation we have to use
relCaching. The difference is that the second uses caching mechanisms for fetched models. Of course you can define your own mechanism but how to do this is not covered in this article.
relCaching needs two functions, the first is like in previous example: responsible for getting categories by its ID, and the second is responsible to get the categories by their relations. When we want to find categories for product, Product’s ID is extracted by
RelationIds and result is passed to
repo.categoriesByProducts where data is read from the database. At the end Relation maps and returns result.
We’re almost done, all we need to do is to expose
categories function for
product field. To define how the
Product is exposed we’ve used
ProductType class. All the fields are extracted by macro, but also have possibility to add more if we need. For such case is
AddFields type class.
What to explain… We just added field with name
categories that expects a list of categories in the response. Resolver uses fetcher, explicitly sets a relation and passes product ID as a parameter for that relation.
That’s it. We did all to set up many-to-many relation. When you’ll do the same work for
Products relation, your code should be similar to ‘stage3’ branch code
If you run code from the branch you should be able to make queries like that:
And get the response:
But what if we will execute following query?
We can analyze complexity of our query and react somehow when it reaches the limit. To give you an example: Add fixed complexity value for every field and when complexity crosses some threshold, it will respond with an error. I chose to make fixed values, but if you want, you can compute the value somehow, for example based on amount of returned records, etc.
Firstly we’ll define helper function and use it in fields
complexity field needs parameter of type
Option[(Ctx, Args, Double) => Double] where
Ctx in our example is
ShopRepository but if you want, you can keep it generic. Because query is tree-like, we compute complexity down the tree recurrently. Now populate some values along defined fields like:
Now our exposed model looks like:
We also set complexity of value 30 to
CategoryType. Ok, but server has to react somehow when complexity reaches value of 300, for example. Such operation could be managed by query reducers. We can create our own
QueryReducer, that checks value of complexity of query, and in case it’s greater than 300 it will throw an exception. Then we will catch the exception and respond with proper status and message.
Let’s begin from implementing Exception then:
Now query reducer responsible for checking the complexity’s value:
At the and we have to tell the
Executor about our handler and reducer. Note that you can define as many reducers as you want. Add to our
This is what we wanted to achieve. Entire article was about implementing read operations, now I want to show you how to implement really simple write operation.
So far we’ve used
query for getting data. If we want to add or update data we have to use
Assume you want to add
Category with name “Foo”, you have to execute such query:
Note: my slick setup doesn’t support autoincrementing of ID, so you have to provide it while adding a record. Of course, this wouldn’t be necessary for a production ready system.
Let’s implement this, starting from database operation. The query above shows, that we have to add a Category, providing id and name. It also expects
Category in return. Following function will manage this:
Time to update the schema. Instead adding another field to
QueryType we have to implement a new ObjectType responsible for all mutations.
We’re reading arguments
name and then we pass these to the proper repository function. Easy like this. The last thing is to tell exposed
Schemaobject that we also support mutations.
That’s all. Now, such prepared server should supports queries like this:
After that we should see “FOO” in the categories listing.
In this article I wanted to show how to implement basic GraphQL server in Scala. I deliberately added some extra effort to show a little more complicated model than you can find in official examples. But it’s still only tip of the iceberg what Sangria provides.
Websocket based subscriptions, Middleware, authorization, caching to name but a few. Read the official guide to find out all the things this library offers. I know I didn’t cover all the possibilities and responded for all beginner’s question, but I hope I encouraged you to go deeper in this topic. Leave a comment for all the questions feedback :D