Let's make a GraphQL client in Elm!

I’m a big fan of GraphQL and I’d like to explore how to use it in Elm. With a statically typed language (like Elm) we should be able to achieve very nice things, such as using a DSL to write queries and determine their correctness at compile time. We’ll initially build a simple implementation and see where that takes us.

I’m going to use a service called graphqlhub which exposes some famous REST apis as a GraphQL endpoint. We’ll use Hacker News for our experiments.

Anatomy of a query

I’ll worry about the dsl and getting a fancy api to build queries later, for now let’s make sure we’re able to send a query and get some result back.

It can be a little bit daunting to wrap your head around json encoders and decoders in Elm, but once you get the hang of it, you’ll realize it is not that hard. If you need a refresher on HTTP and how to send requests in Elm, check out my Rock Paper Scissors post where I go into this stuff in a little bit more detail.

With that out of the way, let’s see how we can query data with GraphQL. As you might already know, the query is either encoded in the body or as a query parameter. We’ll use the latter as we don’t need to mutate anything on the server (that is, we’re only reading, not writing) so a simple GET request is fine.

Try adding the title field to the hnTopStories query below and see how the response changes ;)

Great, so we can get a simple string from the server! What you see are the ids of the top stories on Hacker News right now. But what just happened? Let’s get through it (I encourage you to play with the code and run it again if you’re not sure what’s going on).

type alias Model =
    { response : String
    }


type Msg
    = FetchHNTopStories (Result Http.Error String)

First of all, we declare our Model and Msg types. The model is just the raw json we get from the server (we’ll decode it in a bit) and FetchHNTopStories is our only message, that will get dispatched by the runtime when the response comes back from the server.

hnTopStories : String
hnTopStories =
    """
    {
      hn {
        topStories {
          id
        }
      }
    }
    """

This is the GraphQL query that we’re sending. Note that it’s just a string, far from ideal. We’d like to use a DSL or something like that to create our queries, so that they can be type checked and we can be absolutely sure at compile time that they match the schema on the server (that would require knowing the schema in advance, but let’s not get ahead of ourselves :P).

request : Http.Request String
request =
    let
        encoded =
            Http.encodeUri hnTopStories
    in
        Http.getString ("https://www.graphqlhub.com/graphql?query=" ++ encoded)


init : ( Model, Cmd Msg )
init =
    { response = "Waiting for a response... " } ! [ Http.send FetchHNTopStories request ]

The actual request is pretty simple. As we said before, the query is sent as a query parameter. Note that it has to be encoded. Once we create our Request object, we can create a Cmd through Http.send so that the runtime can perform the appropriate side effects (making the request) and give us back the result through the FetchHNTopStories message.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchHNTopStories (Ok res) ->
            { response = res } ! []

        FetchHNTopStories (Err res) ->
            { response = toString res } ! []


view : Model -> Html Msg
view model =
    div [] [ text model.response ]

The update and view functions are fairly straightforward, nothing ground breaking is happening here.

Decoding the GraphQL response with elm-decode-pipeline

So we’re getting back the ids of the top stories on Hacker News right now. But what we’re seeing is just a giant string that we need to decode, so let’s get a bit more realistic and decode the response into a List Story. Here’s our Story type.

type alias Story =
    { id : String
    , title : String
    }

Right now, we’re only interested in id and title. There are a lot more fields that we can query, we’ll do that later. Let’s update our query to match our brand new type.

hnTopStories : String
hnTopStories =
    """
    {
      hn {
        topStories {
          id
          title
        }
      }
    }
    """

I briefly touched upon decoders in a previous tutorial. That stuff was pretty simple though, when you want to get serious about decoding, you’ll want to use a package such as elm-decode-pipeline. A decoder for our Story type might look like this:

import Json.Decode as Decode
import Json.Decode.Pipeline as Pipeline

-- ...

storyDecoder : Decode.Decoder Story
storyDecoder =
    Pipeline.decode Story
        |> Pipeline.required "id" Decode.string
        |> Pipeline.required "title" Decode.string

I decided to keep everything explicit (that is, using qualified names for the modules Decode and Pipeline) but if you find it easier, you can import the functions you need and drop the module name. I like the clearness of this approach to be onest. This decoder will take a JSON string and decode it to a Story type (if successful). Next, we’ll want to update our request function so that we don’t get back a raw string but a List Story (note we changed the signature and the function getString in get, which allows us to specify a decoder for the response).

request : Http.Request (List Story)
request =
    let
        encoded =
            Http.encodeUri hnTopStories

        decoder =
            Decode.at [ "data", "hn", "topStories" ] <|
                Decode.list storyDecoder
    in
        Http.get ("https://www.graphqlhub.com/graphql?query=" ++ encoded) decoder

Now we need to update our Model, Msg, update and view functions because we’re not handling a String anymore but a List Story, so let’s do that! This is mostly mechanical, should be fairly easy to follow.

type alias Model =
    { stories : List Story
    , message : String
    }

type Msg
    = FetchHNTopStories (Result Http.Error (List Story))


init : ( Model, Cmd Msg )
init =
    { stories = []
    , message = "Waiting for a response... "
    }
        ! [ Http.send FetchHNTopStories request ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchHNTopStories (Ok stories) ->
            { model | stories = stories, message = "Got stories!" } ! []

        FetchHNTopStories (Err res) ->
            { model | message = toString res } ! []

Nice. Up next, the view! It will show a message according to the request status and will display a list of stories once we get some data back. In my eyes (listItem << .title) looks pretty neat. If you’re unsure about what that means, the extended version would be something like: \story -> listItem story.title. We’re being fancy and smart here given that we can express ourselves in a better and shorter way in Elm.

listItem : String -> Html Msg
listItem str =
    li [] [ text str ]


view : Model -> Html Msg
view model =
    let
        items =
            List.map (listItem << .title) model.stories

        storiesList =
            ul [] items
    in
        div [] [ text model.message, storiesList ]

What we’ve done so far is embedded below. Try to run it a second time in case you missed the initial message, so you get a good idea of what’s going on.

Now, let’s say we want to add more fields to our query. We can keep updating the static string that we have, but I think we can do better because it’s getting a little bit unwieldy. So let’s think about how to improve it.

New types

We need at least a couple of types to make things nicer. First, we’d like to define a type Field which represents a field inside a query (in GraphQL speech hn, topstories and id are all fields). They can have arguments, but let’s start simple and pretend they don’t. Here’s how I’d go about defining the Field type.

type alias Field =
    { name : String
    , query : Query
    }

Notice that we’re introducing a new type, Query. This time, it won’t be a type alias because that would not make any sense to the compiler, as it’s a recursive type (Field is defined in terms of Query and vice versa). So it will need to be a concrete type, fair enough.

type Query
    = Query (List Field)

A Query holds the eventual children of aField. We’ll convert our current query into a Query in a bit so that what we’re trying to do becomes clearer.

Now, how do we go about putting together a query with our new found types? It’s easy, really, let’s define an helper function field to make things more readable.

field : String -> List Field -> Field
field name fields =
    Field name (Query fields)

And then use it to create the query we had before.

topStoriesQuery : Query
topStoriesQuery =
    Query
        [ field "hn"
            [ field "topStories"
                [ field "id" []
                , field "title" []
                ]
            ]
        ]

This is kind of the same thing as building an Html tree in Elm – the last argument (the Query in our case) represents the children of a Field. Sounds good? Great, let’s move on to converting it into a string so that we can actually use it then! How do we convert a Field into a String? Not that difficult if you think about it, we just use the field name and append it to its (possibly empty) query.

fieldToString : Field -> String
fieldToString { name, query } =
    name ++ " " ++ queryToString query

The last thing we need is the queryToString function – it turns a Query value into a String that we can send to the server.

queryToString : Query -> String
queryToString (Query query) =
    if List.isEmpty query then
        ""
    else
        let
            str =
                List.map fieldToString query
                    |> List.foldr (++) ""
        in
            "{ " ++ str ++ " }"

With our brand new functions, we can go on and update the request function so that it uses the topStoriesQuery that we just defined.

request : Http.Request (List Story)
request =
    let
        encoded =
            queryToString topStoriesQuery
                |> Http.encodeUri

        decoder =
            Decode.at [ "data", "hn", "topStories" ] <|
                Decode.list storyDecoder
    in
        Http.get ("https://www.graphqlhub.com/graphql?query=" ++ encoded) decoder

The result should be the same, but the refactoring we did is instrumental to get to our goal mentioned at the beginning of the post!

Extending the example

Just for the sake of having something a little bit cooler to play with, let’s extend the Story type to include the url. The api will return null when the story is a text post (an Ask HN or Show HN kind of post) so we’ll need to make it optional. We’re going to do that by declaring it as a Maybe String in our model and decoding it with Json.Pipeline.optional defaulting to Nothing.

type alias Story =
    { id : String
    , title : String
    , url : Maybe String
    }

-- ...

topStoriesQuery : Query
topStoriesQuery =
    Query
        [ field "hn"
            [ field "topStories"
                [ field "id" []
                , field "title" []
                , field "url" [] -- we need the url
                ]
            ]
        ]

-- ...

storyDecoder : Decode.Decoder Story
storyDecoder =
    Pipeline.decode Story
        |> Pipeline.required "id" Decode.string
        |> Pipeline.required "title" Decode.string
        |> Pipeline.optional "url" (Decode.nullable Decode.string) Nothing -- url might be null

With that done, we can change the view to link to the actual stories, defaulting to the Hacker News link if it’s a text post.

listItem : Story -> Html Msg
listItem { id, title, url } =
    let
        url_ =
            Maybe.withDefault ("https://news.ycombinator.com/item?id=" ++ id) url
    in
        li []
            [ a [ href url_ ] [ text title ]
            ]


view : Model -> Html Msg
view model =
    let
        items =
            List.map listItem model.stories

        storiesList =
            ul [] items
    in
        div [] [ text model.message, storiesList ]

Find below the result, you can play with it and edit it as much as you like straight from your browser!

Wrapping up

This was intended to be part one of my journey with Elm + GraphQL. There are some nice libraries out there but I want to see for myself what can be accomplished with these two technologies (my gut says a lot). I have some more stuff ready that I want to share, next time we’ll dig deeper into the rabbit hole… until then, if you enjoyed this article you can follow me on Twitter ;)