Cookies
Diese Website verwendet Cookies und ähnliche Technologien für Analyse- und Marketingzwecke. Durch Auswahl von Akzeptieren stimmen Sie der Nutzung zu, alternativ können Sie die Nutzung auch ablehnen. Details zur Verwendung Ihrer Daten finden Sie in unseren Datenschutz­hinweisen, dort können Sie Ihre Einstellungen auch jederzeit anpassen.

Engineering

RestQL - Using GraphQL for building REST APIs
Minuten Lesezeit
Blog Post - RestQL - Using GraphQL for building REST APIs
Ruben Grimm

A (very) brief introduction to GraphQL

GraphQL describes itself as a "query language for your API". We live in a web-driven world where the term API often translates to Web-API. But despite that, it is worth noting, that GraphQL is not only capable of powering APIs through HTTP, but it is a query language comparable to SQL, that can power any kind of Application Interface. The general idea behind GraphQL is, that a client defines which data to receive from the API by passing a GraphQL document to it. I won't get into the details of GraphQL here. If you want more information on this you can find an introduction on our blog: An Introduction to GraphQL or have a look at the official tutorials and documentations.

Implementing a simple RESTful API for a blog

To implement our REST API we'll be using Elixir and Phoenix. I got to know GraphQL through Absinthe, the GraphQL Toolkit for Elixir, and I just love it. But there are of course implementations for many different programming languages including Ruby and PHP.

So following this little experiment shouldn't be a big problem if you know some web framework like Phoenix, Rails, Laravel or similar.

The general idea: GET /posts

A typical implementation of the index function for a REST API would look something like this:

defmodule Restql.Web.PostController do
  use Restql.Web, :controller

  def index(conn, _params) do
    posts = Restql.Blog.list_posts
    json conn, posts
  end
end

Now we change this implementation, so that it sends a GraphQL document to our internal API implementation.

defmodule Restql.Web.PostController do
  use Restql.Web, :controller

  alias Restql.Blog
  alias Restql.Blog.Post

  def index(conn, _params) do
    graphql conn, """
      {
        posts {
          title
          body
          author {
            name
          }
        }
      }
    """
  end

  defp graphql(conn, document, variables \\ %{}, schema \\ Restql.Blog.Schema) do
    with {:ok, data} <- Absinthe.run(document, schema, variables: variables) do
      json conn, data
    end
  end
end

For that we define a private function graphql that takes a connection and a GraphQL document, which is nothing more than a string, runs it against a GraphQL schema via Absinthe and sends the result as a JSON response.

Now, for our GraphQL API to work, we need three things:

  1. A type for every object we want to be retrievable.
  2. A schema that defines the queries and mutations our API can resolve.
  3. A resolver for every query and mutation, that does the actual job.

So our RestQL implementation will have the following structure.

RestQL

At first we need types for the posts and users (author) we want to receive.

defmodule Restql.Blog.Schema.Types do
  use Absinthe.Schema.Notation

  object :post do
    field :id, :id
    field :title, :string
    field :body, :string
    field :author, :user
  end

  object :user do
    field :id, :id
    field :name, :string
  end
end

Next we define the query posts in our schema.

defmodule Restql.Blog.Schema do
  use Absinthe.Schema
  import_types Restql.Blog.Schema.Types

  query do
    field :posts, list_of(:post) do
      resolve &Restql.Blog.PostResolver.all/2
    end
  end
end

The schema imports the types we implemented earlier and defines the query posts. This query will be resolved by the function PostResolve.all/2, which does the actual work of retrieving the posts and the corresponding author.

defmodule Restql.Blog.PostResolver do
  def all(_args, _info) do
    {:ok, Restql.Blog.list_posts |> Restql.Repo.preload(:author)}
  end
end

Now we can run our server and will be able to retrieve all posts by visiting GET /posts. The API will provide the title, the body and the author name forall posts. If we want it to provide the post's id too, we just need to add it to our GraphQL query document in our PostController, since we already defined it as a field on the post object for GraphQL.

Using manipulators - POST /posts

In the same way we can now define all other REST actions using GraphQL documents. I'll show this for the creation of new blog posts. First we need to define a create function on our PostController as we would do in a typical REST implementation.

defmodule Restql.Web.PostController do
  # ...
  def create(conn, %{"post" => post_params}) do
    graphql conn, """
      mutation CreatePost($title: String!, $body: String!) {
        post(title: $title, body: $body) {
          id
        }
      }
    """, post_params
  end
  # ...
end

As you can see we now use a mutation to create a new post. For that we put the post parameters, passed to the controller, into the document via GraphQL variables. This way the parameters get sanitized and we can even check for their type and presence with String!. Note that GraphQL provides a very extensive type system, that we could use here, if we wanted.

So all we need to do now, is to define this manipulation in our schema and implement the resolver:

defmodule Restql.Blog.Schema do
  # ...
  mutation name: "CreatePost" do
    field :post, type: :post do
      arg :title, non_null(:string)
      arg :body, non_null(:string)
      resolve &Restql.Blog.PostResolver.create/2
    end
  end
  # ...
end
defmodule Restql.Blog.PostResolver do
  # ...
  def create(args, _info) do
    Restql.Blog.create_post(args)
  end
  # ...
end

We defined title and body to be non-null strings. By that we could have avoided checking for presence and type via document variable types. But keep in mind, that the errors, the client will receive, won't really make sense to him, since it will reference the GraphQL document, he doesn't know about. It would be easy to handle this error in our controller and provide a useful error message, though.

So know our RestQL API is ready to ship. But what are the take-aways from this experiment?

Why all the fuss?

Couldn't we just have implemented the same thing without all this fancy GraphQL knick-knack? Well, of course we could have and I'm aware, that using GraphQL to power a small RESTful API like this, may not make sense at all.

But although this was more of an experiment, I can think of some reasons to use this pattern in production.

1. Useful patterns

Using Absinthe to implement a GraphQL API is just smooth. I like seperating my API implementation into a schema, types and resolvers. Of course this isn't that different to a well-made API implementation using short controller functions, separated contexts and models, but with Absinthe you are forced to do so and may end up with more readable code.

2. Easy to change

Your client wants to have this or that information hidden, shown, transformed? Most changes to the API won't require much, you'll never have to remove fields from your GraphQL types once you put them there. Sometimes the change will be as small as changing the GraphQL document in the controllers.

3. Easy to create different APIs for different clients

You want to provide slightly different APIs for different clients i.e. send less data to mobile devices? With a GraphQL backend you can do that with only one API implementation while changing the controller facade that lays on top of that.

4. Use caching, set up RESTful API and GraphQL on different servers, get fancy

We created the GraphQL documents just by writing some Elixir strings. When building bigger APIs one would probably want to use a GraphQL client library for that (for Elixir there is Neuron). When those libraries mature, you might get caching strategies, to reduce the hits on your database. You could even think about splitting the GraphQL backend and the RESTful API to different apps and get fancy with distributed setups and what not.

Conclusion: When to use and when most definitely not to

"Nice, now I get to refactor all my APIs!"
— no one ever

Well, you don't. I listed some reasons, this pattern may make sense for some cases, but it won't make sense for most, where proven RESTful patterns just work.

On the other hand, if you are interested in GraphQL or even think about providing a GraphQL API over HTTP besides a RESTful API, this pattern will probably make sense. And with GraphQL client libraries becoming more mature, providing RESTful APIs with a GraphQL backend may become extremely useful.

Du suchst den richtigen Partner für erfolgreiche Projekte?

Lass uns ins Gespräch kommen.