'How to Create a GraphQL API with Scala and Sangria' post illustration

How to Create a GraphQL API with Scala and Sangria

avatar

If you're thinking about trying out GraphQL in your Scala application in a bid to optimize the network throughput, then you picked out the right time. The technology has matured a lot since 2012, and we can use a great GraphQL implementation for Scala called Sangria. But how do you develop a Scala app with it?

For starters, it's necessary to have a basic understanding of GraphQL. So, we first discuss the key aspects of creating a GraphQL API. After that, we look at how we implemented our sample Scala application with GraphQL. The application allows you to save and retrieve posts and you can have a look at the final result in this repository:

The technology stack of our application includes the following:

  • Scala 2.12
  • Play Framework 2.7
  • SBT
  • Guice
  • Sangria 1.4

Here's also the application structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scala-graphql-api
├── app                    # The Scala application's source code
│   ├── controllers        # A solitary AppController
|   ├── errors             # Error classes
│   ├── graphql            # GraphQL main and concrete schemas and resolvers
│   │   ├── resolvers      # Resolver methods to retrieve data from the database
│   │   ├── schemas        # Concrete schemas, in particular, PostSchema
│   │   └── GraphQL.scala  # Defines application-wide GraphQL-related objects
│   ├── models             # The models, in particular, a Post model 
│   ├── modules            # The modules such as PostModule and DBMobule
│   ├── repositories       # The traits PostRepository and its implementation
│   └── views              # HTML layouts (a graphiql layout)
├── conf
├── project
├── public
├── test
├── .gitignore
├── .travis.yml
├── build.sbt
└── README.md

Notice that all the code related to GraphQL is stored under the app/graphql directory.

Here's also the Post entity we work with:

1
case class Post(id: Option[Long] = None, title: String, content: String)

Next, we walk you through GraphQL and Sangria. You can just skip to the Creating Scala application with Sangria section, though, if you already have general understanding of GraphQL.

What's GraphQL?

In simple terms, the graphical query language, or GraphQL, helps describe the data transferred between the client and server and the operations you can perform with that data. Compared to the RESTful design, with GraphQL you get a simple way to send only the concrete object fields upon request from the client application and avoid sending huge objects with fields that might never be used.

What’s special about GraphQL queries is that they consist of these three components:

  • query, contains the actual GraphQL query or mutation
  • operationName, contains the name of the incoming query or mutation (if there's a name)
  • variables, contain the dynamic values that can be passed with the query; these can be query parameters with data to store in the database

The response from a GraphQL API will also be JSON but with the fields data and errors.

The field data contains the requested object or multiple objects, for instance, { "data": { "posts": [ post1, post2, post3 ] }}. As for the field errors, it's only added to the response body when a GraphQL query or mutation cannot be handled. For example, if an incoming GraphQL request was built incorrectly, the GraphQL API will add errors to the body and send it to the client.

We've just mentioned what kinds of operations we can perform on data when using GraphQL, but let's make clear what they really mean:

  • Queries, requests with or without parameters to get data from the server. Think of GET in REST.
  • Mutations, requests with parameters to create, delete, or update data (think of PUT, POST, or DELETE).

There's another type of GraphQL query that the client application can send — subscriptions. We're focusing on GraphQL basics, though, so we don't talk about subscriptions yet.

How does Sangria fit into the high-level explanation we gave on GraphQL? Sangria provides the actual classes and methods to build GraphQL schemas and parse and execute GraphQL queries and mutations.

Here are a few classes from Sangria that we’re going to use:

  • QueryParser, lets our Scala app parse GraphQL requests to understand what's requested
  • Schema, lets the app determine the shape of objects and how to handle queries and mutations
  • Executor, executes incoming queries and mutations according to the created schema

As a final step before discussing the implementation of a Scala GraphQL API, let's clarify the application flow from the client to the server when using Sangria.

The flow of actions from the client application to a backend API built with Scala and Sangria

  1. The client application sends a GraphQL query or a mutation (or a subscription).
  2. A Play controller parses and executes the incoming request using Sangria's executor.
  3. The executor maps a GraphQL query to a schema and then calls a respective resolver.
  4. A resolver gets query parameters and then requests the database to return data to the executor.
  5. The executor gets the data and sends it back to the client application.

Armed with this high-level overview of how a Scala application uses GraphQL with the help of Sangria, we can now focus on the application.

Creating a Scala application with Sangria and GraphQL

We first want to take a look at our main and only controller defined in app/AppController.scala, which defines a few methods to handle GraphQL queries. Thanks to GraphQL, you don't need to create multiple controllers, as you would do in a RESTful application.

The controller first defines the method graphiql() whose only job is to respond with the GraphiQL HTML page to provide a graphical interface you can use to send queries and mutations to the backend API. You won't really need to use this method in a real application. At the end of this article, however, we give a few examples of queries and mutations you can run in GraphiQL to test the application.

Next, you'll see graphqlBody(), the method that parses and extracts the GraphQL body, checks if a GraphQL request was built correctly, and calls the method executeQuery() to handle the query. Otherwise, graphqlBody() throws an exception and returns BadRequest.

Here's the entire graphqlBody() implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def graphqlBody: Action[JsValue] = Action.async(parse.json) {
  implicit request: Request[JsValue] =>

    val extract: JsValue => (String, Option[String], Option[JsObject]) = query => (
      (query \ "query").as[String],
      (query \ "operationName").asOpt[String],
      (query \ "variables").toOption.flatMap {
        case JsString(vars) => Some(parseVariables(vars))
        case obj: JsObject => Some(obj)
        case _ => None
      }
    )

    val maybeQuery: Try[(String, Option[String], Option[JsObject])] = Try {
      request.body match {
        case arrayBody@JsArray(_) => extract(arrayBody.value(0))
        case objectBody@JsObject(_) => extract(objectBody)
        case otherType =>
          throw new Error {
            s"The '/graphql' endpoint doesn't support a request body of the type [${otherType.getClass.getSimpleName}]"
          }
      }
    }

    maybeQuery match {
      case Success((query, operationName, variables)) => executeQuery(query, variables, operationName)
      case Failure(error) => Future.successful {
        BadRequest(error.getMessage)
      }
    }
}

The key function is extract(), which accepts the body of the incoming request and gets the query, operationName, and variables that came with it. As we explained earlier, query contains the actual GraphQL query; it can be a GraphQL query or mutation type, so it's always included in the GraphQL body. The other two properties, operationName and variables, are optional and may not be provided. All three values are eventually passed to the executeQuery() method.

We finally reach the key method that actually executes GraphQL queries — executeQuery(). It uses two Sangria's classes: QueryParser to parse GraphQL queries and Executor to execute them against a schema.

1
2
3
4
5
6
7
8
9
10
11
12
def executeQuery(query: String, variables: Option[JsObject] = None, operation: Option[String] = None): Future[Result] = QueryParser.parse(query) match {
 case Success(queryAst: Document) => Executor.execute(
   schema = graphQL.Schema,
   queryAst = queryAst,
   variables = variables.getOrElse(Json.obj()),
 ).map(Ok(_))
   .recover {
     case error: QueryAnalysisError => BadRequest(error.resolveError)
     case error: ErrorWithResolver => InternalServerError(error.resolveError)
   }
 case Failure(ex) => Future(BadRequest(s"${ex.getMessage}"))
}

Note that we must pass a global GraphQL schema to Executor as the first parameter: schema = graphQL.Schema. A global schema contains the information about all GraphQL schemas created in a Scala application.

Other than the schema, Executor needs queryAst and variables. The queryAst parameter is a type of object that's returned by QueryParser if the GraphQL request was successfully parsed. variables are the parameters that the client can send to your backend application. For example, if the client wants to GET a specific post, then the variables property might have an ID or some other data of the required post.

So what actually happens in executeQuery()? The executor looks at the schema and compares the incoming query or mutation with what's described in the schema. If there's a match, then the executor runs a respective resolver method.

Next, we should discuss what the schema looks like and what are resolvers. Let's start with the schema.

Building a GraphQL schema for a Scala application

GraphQL requires us to build a schema. A schema let's us define object types that we're going to send to the client application, whereas queries and mutations specify to what requests the application must answer with what data. And object types are actually used in queries and mutations.

Let's have a look at the global GraphQL schema that we created for our Scala application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package graphql

import com.google.inject.Inject
import graphql.schemas.PostSchema
import sangria.schema.{ObjectType, fields}

class GraphQL @Inject()(postSchema: PostSchema) {

 val Schema = sangria.schema.Schema(
   query = ObjectType("Query",
     fields(
       postSchema.Queries: _*
     )
   ),

   mutation = Some(
     ObjectType("Mutation",
       fields(
         postSchema.Mutations: _*
       )
     )
   )
 )
}

As you can see, there are three building blocks provided by Sangria to let us build GraphQL schemas — Schema, ObjectType, and fields.

Our global schema accepts two parameters, query and mutation, that are defined as ObjectType with their respective types — "Query" and "Mutation". Inside ObjectType definitions, we have fields mapped to the queries and mutations of a concrete schema PostSchema. You can add other schemas by defining new fields inside ObjectType. Your concrete GraphQL schema can have either query or mutation or both, or it may even have a subscription field if you're going to use GraphQL subscriptions.

The concrete schemas define the queries and mutations for specific entities.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package graphql.schemas

import com.google.inject.Inject
import graphql.resolvers.PostResolver
import models.Post
import sangria.macros.derive.{ObjectTypeName, deriveObjectType}
import sangria.schema._

/**
  * Contains the definitions of all queries and mutations
  * for the Post entity. PostSchema is also a building block
  * for the global GraphQL schema.
  *
  * @param postResolver an object containing all resolver functions to work with the Post entity
  */
class PostSchema @Inject()(postResolver: PostResolver) {

  /**
    * Сonverts a Post object to a Sangria GraphQL object.
    * The Sangria macros deriveObjectType creates an ObjectType
    * with fields found in the Post entity.
    */
  implicit val PostType: ObjectType[Unit, Post] = deriveObjectType[Unit, Post](ObjectTypeName("Post"))

  /**
    * Provide a list of queries to work with the Post entity.
    */
  val Queries: List[Field[Unit, Unit]] = List(/* ... */)

  /**
    * Provide a list of mutations to work with the Post entity.
    */
  val Mutations: List[Field[Unit, Unit]] = List(/* ... */)
}

The code above is self-explanatory, but let's discuss a few keys aspects of a schema definition.

First, we define the implicit value PostType and we use a couple of methods from Sangria — deriveObjectType and ObjectTypeName. The deriveObjectType() method derives GraphQL types from normal Scala classes. Simply put, it just converts a post object to GraphQL ObjectType.

After that, we describe queries and mutations for the entity Post. We've omitted the actual implementations of the Queries and Mutations for now, but we'll look at them further below.

Our concrete GraphQL schema also needs resolvers, which are just methods that retrieve data from the database or a third-party API and return it to the executor. This is why we imported PostResolver; we also look at resolvers later in this guide.

Let's turn our attention to what the Queries variable looks like in a GraphQL schema. Basically, the variable stores a list of fields, and each field has these three properties:

  • name such as posts or findPost
  • fieldType such as ListType(PostType) or OptionType(PostType)
  • resolve, which is a resolver method that gets data from a database or third-party API

The code sample below shows the Queries implementation we created for our Scala application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val Queries: List[Field[Unit, Unit]] = List(
  Field(
    name = "posts",
    fieldType = ListType(PostType),
    resolve = _ => postResolver.posts
  ),
  Field(
    name = "findPost",
    fieldType = OptionType(PostType),
    arguments = List(
      Argument("id", LongType)
    ),
    resolve =
      sangriaContext =>
        postResolver.findPost(sangriaContext.args.arg[Long]("id"))
  )
)

How do you read this code? Say, a client application sends a GraphQL query posts to get all posts. Sangria's Executor (in AppController) finds a proper schema in the global GraphQL schema for posts. And since posts is defined in PostSchema, then the registered resolver for posts will run. The same way it works for other queries and mutations.

Notice that sangriaContext can be passed to a resolve method. The context is used when the client application wants to get specific data, for example, a concrete post. So the client app must pass an ID or other parameter to the query. The ID is then retrieved from the arguments stored in sangriaContext and passed to a respective method to get the necessary data.

Mutations look very much the same as queries. You specify a list of fields with the name, fieldType, arguments, and a resolve method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
val Mutations: List[Field[Unit, Unit]] = List(
  Field(
    name = "addPost",
    fieldType = PostType,
    arguments = List(
      Argument("title", StringType),
      Argument("content", StringType)
    ),
    resolve = sangriaContext =>
      postResolver.addPost(
        sangriaContext.args.arg[String]("title"),
        sangriaContext.args.arg[String]("content")
      )
  ),
  Field(
    name = "updatePost",
    fieldType = PostType,
    arguments = List(
      Argument("id", LongType),
      Argument("title", StringType),
      Argument("content", StringType)
    ),
    resolve = sangriaContext =>
      postResolver.updatePost(
        sangriaContext.args.arg[Long]("id"),
        sangriaContext.args.arg[String]("title"),
        sangriaContext.args.arg[String]("content")
      )
  ),
  Field(
    name = "deletePost",
    fieldType = BooleanType,
    arguments = List(
      Argument("id", LongType)
    ),
    resolve =
      sangriaContext =>
        postResolver.deletePost(sangriaContext.args.arg[Long]("id"))
  )
)

Since mutations are used to mutate data, you need to specify the arguments field and provide a list of arguments that the client application must pass to save or update the data. Hence, each mutation receives its own set of arguments to carry out a specific task.

There's one more GraphQL feature we need to discuss — resolvers. What do they do exactly? The next section sheds light on them.

GraphQL resolvers

In this section, we discuss just one resolver to give you an idea how they work. Here's the code of a GraphQL resolver stored under the app/graphql/resolvers directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package graphql.resolvers

import com.google.inject.Inject
import models.Post
import repositories.PostRepository

import scala.concurrent.{ExecutionContext, Future}

/**
  * A resolver class that contains all resolver methods for the Post model.
  *
  * @param postRepository   a repository that provides the basic operations for the Post entity
  * @param executionContext executes the program logic asynchronously, typically but not necessarily on a thread pool
  */
class PostResolver @Inject()(postRepository: PostRepository,
                             implicit val executionContext: ExecutionContext) {

  /**
    * Returns a list of all posts.
    *
    * @return a list of posts
    */
  def posts: Future[List[Post]] = postRepository.findAll()
}

PostResolver methods actually get the data from the database or an API.

Note, however, that we want to keep our application easy to extend and we don't write the database or API queries directly in a resolver class. Instead, we use a repository. In this case, our Scala application has PostRepository, which is just a trait stored under app/repositories with its implementation PostRepositoryImpl. Traits are used to separate the database queries from the schema to keep our code clean and simple.

Try out a Scala API with Sangria

Here's the summary of how a Scala API with a GraphQL endpoint understands how to react to different queries:

  1. AppController gets a query or mutation and calls an executor.
  2. The executor runs through the schema, finds a suitable query or mutation, and runs a resolver.
  3. The resolver fetches the data from the database or a third-party API.
  4. The resolver returns the fetched data to the executor.
  5. The executor sends the data to the client app.

You can clone our Scala, Sangria, and Play project from GitHub and run it using the command sbt run. Naturally, Java must be installed on your computer.

Once the project is built, you can open your favorite browser at http://localhost:9000/. The GraphiQL interface will be loaded, and you can send a few queries and mutations to the Scala application:

Running a mutation to the Scala GraphQL API built with Sangria

Here are a few examples of queries and mutations that you can try out:

  • Create your first post with a title and content. The mutation returns the post ID, title, and content properties:
1
2
3
4
5
6
7
mutation {
  addPost(title:"Some Post" content:"First post") {
    id
    title
    content
  }
}

Your application doesn't have to return all the fields of a new object. It's enough to just remove the unnecessary fields to get only the post title without creating another resolver:

1
2
3
4
5
mutation {
  addPost(title:"Some Post", content:"First post") {
    title
  }
}
  • A query that finds a specific post by its ID:
1
2
3
4
5
6
7
query {
  findPost(id: 1) {
    id
    title
    content
  }
}
  • A query that returns a list of all posts with the ID, title, and content fields:
1
2
3
4
5
6
7
query {
  posts {
    id
    title
    content
  }
}
  • A mutation that updates an existing post with the ID 1:
1
2
3
4
5
6
7
mutation($id: ID, $) {
  updatePost(id: 1, title: "Some title", content: "Any content") {
    id
    title
    content
  }
}
  • A mutation that removes a post with the ID 1 if it exists:
1
2
3
mutation {
  deletePost(id: 1)
}

That's how you build a Scala application using Sangria and GraphQL. Ask questions in the comments section if you want to clarify anything.

If you're looking for a developer or considering starting a new project,
we are always ready to help!