ReasonML with GraphQL, the Future of Type-Safe Web Applications

A Superb Pairing.

ReasonML, also known as JavaScript-flavored OCaml, offers nearly impenetrable type safety for developing user interfaces. By adopting a static type system, you can eliminate an entire class of errors before your app is served.

We’re going to look into building a small web application that uses consumes a GraphQL endpoint using ReasonML. If you’d like to check out the finished example, here is the repo.

We’ll cover:

  1. Getting started with a ReasonReact project
  2. Setting up a client with reason-apollo
  3. Sending queries
  4. Mutating data
  5. And some of the quirks and syntactic funsies of ReasonML

If you’re new to both GraphQL and ReasonML, I’d suggest learning one at a time. Learning more than one big thing at once is often difficult for me.

If you’re experienced with JavaScript and GraphQL, but want to learn ReasonML, read on, but keep the docs handy.

Getting Started — Establishing a ReasonReact Project

In order to get going with ReasonML, we must first install the cli, bs-platform that handles bootstrapping the project. You should also get an editor plugin that helps with developing ReasonML apps. If you’re using VSCode, reason-vscode by Jared Forsyth is my preferred plugin.

npm install -g bs-platform

This installs the BuckleScript compiler that turns our ReasonML into wonderful JavaScript which has already been type-checked and can be run in the browser.

Now we can initialize our project and hop right in.

bsb -init reason-graphql-example -theme react
cd reason-graphql-example
npm install
  • The init argument specifies the name of the project we’re initializing.
  • The theme argument specifies the template we wish to use. I usually just choose the react theme.
  • We run npm install to install dependences just like in any other JavaScript project.

With the project scaffolded, we can try to build it. In two separate terminal panes, run:

npm start
# and
npm run webpack
  • npm start tells BuckleScript (via the bsb command) to build the project watch for changes to your .re files.
  • npm run webpack fires up webpack to build your main JavaScript bundle
Tip: you’ll notice that the webpack output is in the build folder but the index.html file is in the src folder. We can make serving the project a little bit easier by moving the index.html file to the build folder and rewriting the script tag to point at the adjacent Index.js file.

With all that taken care of, you can serve your build folder using http-server build, serve build, or, without a server, open build/index.html and check out the project.

When I’m developing a ReasonML project, I’m running 3 terminal tabs:

  1. npm start to compile ReasonML to JavaScript
  2. npm run webpack to bundle JavaScript
  3. serve build to actually serve the build on a port

Before we can get to the fun stuff, we still must clear out the boilerplate and set up react-apollo.

Go ahead and remove the Component1.re and Component2.re files, and then update Index.re to the following:

ReactDOMRe.renderToElementWithId(<App />, "root");

Update index.html to:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>ReasonML GraphQL Example</title>
</head>
<body>
<div id="root"></div>

<script src="./Index.js"></script>
</body>
</html>

Finally, create an App.re file and add the following:

/* App.re */
let str = ReasonReact.string;
let component = ReasonReact.statelessComponent("App");

let make = _children => {
...component,
render: _self =>
<div>
<h1> {"ReasonML + ReasonReact + GraphQL" |> str} </h1>
</div>
};

Some quick notes on this stateless, functional component:

  • a ReasonReact component, whether it is a stateless or a reducer component, must have its spec defined and spread inside the make function.
  • a component must implement the render method
  • I assign ReasonReact.string to str to create an easier shorthand. It makes the JSX a little less noisy.

You make have to re-run your terminal commands, but with all that said and done, you should have this appear on your screen:

Hello world indeed.

It feels like a lot of effort to get started, but accepting early friction for a smoother experience later on is the trade-off here.

Initializing Reason Apollo

In order to get set up with Apollo we’re going to run:

npm install -S reason-apollo react-apollo apollo-client apollo-cache-inmemory apollo-link apollo-link-context apollo-link-error apollo-link-http graphql graphql-tag apollo-link-ws apollo-upload-client subscriptions-transport-ws

That looks like a big install command.

It is, but only the first package, reason-apollo, is consumed in our ReasonML code. However, reason-apollo is a library of bindings that depends on these other JavaScript packages.

In order to make writing GraphQL queries more friendly, we’ll need one more dev dependency.

npm install -D graphql_ppx

As that installs we can open up our bsconfig.json file and update the “bs-dependencies” and “ppx-flags” keys like so:

// bsconfig.json
{
"bs-dependencies": [
"reason-react",
"reason-apollo"
],
"ppx-flags": [
"graphql_ppx/ppx"
],

// other fields...
}

The “bs-dependencies” array tells BuckleScript to include those npm modules in the build process. The “ppx-flags” array lets our IDE know how to preprocess certain directives.

In our case, it allows us to write GraphQL queries with field validation.

Create a file inside the src folder called Client.re. This is where we will declare our instance of the Apollo Client.

/* Client.re */
let inMemoryCache = ApolloInMemoryCache.createInMemoryCache();

let httpLink =
ApolloLinks.createHttpLink(~uri="https://video-game-api-pvibqsoxza.now.sh/graphql", ());

let instance =
ReasonApollo.createApolloClient(~link=httpLink, ~cache=inMemoryCache, ());
Note: If this uri, https://video-game-api-pvibqsoxza.now.sh/graphql does not work, please send me a message on twitter or here in the comments and I’ll update that as quickly as possible.

When we work with ReasonML, any variable that we create with a let binding is automatically exported from the module for us.

With the instance created, we can reference it in any of our other .re files. Update Index.re to the following:

/* Index.re */
ReactDOMRe.renderToElementWithId(
<ReasonApollo.Provider client=Client.instance>
<App />
</ReasonApollo.Provider>,
"root",
);

It looks a little like a standard React JS application, with a couple caveats. Notice that there are no import statements. In ReasonML, we have access to all of the namespaces built in our application. From the perspective of Index.re, We can see the Client and the App modules.

When we create a .re file in our src folder, it becomes a module. We could also declare our modules explicitly within our files.

It is now time to consume our API.

Sending Queries and Rendering a List

While writing this article I created a small Node GraphQL server, the code of which is available at this repo. To keep costs low, I declared an array of mock data to return on each GraphQL request rather than host a database.

Rather than create a todo app, I decided to create a list of video games that I played at some point long ago. Then, I could check if I finished it or not, thus remembering the games that I still haven’t beat.

As we are working with a GraphQL server, we should be able to figure out exactly how to call it by observing the schema.

type VideoGame {
id: ID!
title: String!
developer: String!
completed: Boolean!
}

type Query {
videoGames: [VideoGame!]!
}

type Mutation {
completeGame(id: ID!): VideoGame!
}

Currently, we have one query and one mutation, both of which operate around this VideoGame type. A GraphQL adept will notice that every return value is non-nullable, that is, these responses cannot return unset fields or null objects.

graphql_ppx gives us a script called send-introspection-query, which tells ReasonML how to understand our schema by generating a graphql_schema.json file.

With it, the queries we write are type-safe and validated at compile time.

npm run send-introspection-query https://video-game-api-pvibqsoxza.now.sh/graphql

Let’s define our first query in on top of App.re, just below the component declaration.

/* App.re */ 

module VideoGames = [%graphql
{|
query VideoGames {
videoGames {
id
title
developer
completed
}
}
|}
];

module VideoGamesQuery = ReasonApollo.CreateQuery(VideoGames);

/* let make = ... */

Comparing with the JavaScript in react-apollo, this code would be most analogous to:

const VideoGames = gql`
query VideoGames {
videoGames {
id
title
developer
completed
}
}
`

// later in render
render() {
return (
<Query query={VideoGames}> {/* ... */} </Query>
)
}

Now let’s update the render function:

/* App.re */
let make = _children => {
...component,
render: _self => {
let videoGamesQuery = VideoGames.make();
<div>
<h1> {"ReasonML + ReasonReact + GraphQL" |> str} </h1>
<VideoGamesQuery variables=videoGamesQuery##variables>
...{
({result}) =>
switch (result) {
| Loading => <div> {"Loading video games!" |> str} </div>
| Error(error) => <div> {error##message |> str} </div>
| Data(data) => <VideoGameList items=data##videoGames />
}
}
</VideoGamesQuery>
</div>;
}
};
  • we’re taking advantage of ReasonML’s coolest feature — pattern matching, to handle each of the variants that the query response could be.
  • notice the ## operator that is used to access fields on the videoGamesQuery variable and the data variable. These are not ReasonML records, they are JSON, and must be used a little differently.

Say farewell to branches of if-else statements. Pattern matching combined with variants makes the logic more linear and easier to follow.

It also reduces branch checking to constant rather than linear time, making it more efficient.

If the ReasonML code ever seems more verbose, just remember we’re still getting perfect type safety when it compiles. We still need to build the VideoGamesList component as well as define the videoGame record type.

Starting with the record type, create a new file called VideoGame.re and add the following:

/* VideoGame.re */

[@bs.deriving jsConverter]
type videoGame = {
id: string,
title: string,
developer: string,
completed: bool,
};

The videoGame type as we have it here, has 4 fields, none of which are optional. The BuckleScript directive above it adds a pair of exported utility methods that allow us to convert between ReasonML records and JavaScript objects.

Tip: when Apollo returns a response, it returns untyped JavaScript objects. The jsConverter directive gives us an exported method called videoGameFromJs that we can use to map the Apollo query data to fully typed ReasonML.

To see this mechanic in action, create a new file called VideoGameList.re and add:

/* VideoGameList.re */
open VideoGame;

let str = ReasonReact.string;
let component = ReasonReact.statelessComponent("VideoGameList");

let make = (~items, _children) => {
...component,
render: _self =>
<ul style={ReactDOMRe.Style.make(~listStyleType="none", ())}>
{
items
|> Array.map(videoGameFromJs)
|> Array.map(item =>
<li key={item.id}>
<input
id={item.id}
type_="checkbox"
checked={item.completed}
/>
<label htmlFor={item.id}>
{item.title ++ " | " ++ item.developer |> str}
</label>
</li>
)
|> ReasonReact.array
}
</ul>,
};
  1. Open the VideoGame module (VideoGame.re) at the top so we can use all of it’s exports in the VideoGameList module.
  2. Declare the component type and string shorthand.
  3. Define a make function that expects one prop, items.
  4. Inside the render function, pipe the items to convert JS objects to ReasonML records, map records to JSX, and finally output them as an array.

This code block has a few new ReasonML quirks that we haven’t seen yet.

  • we write the props of the component as labeled arguments to the make function. An argument such as ~items is said to be labeled if it has a ~. Unlabeled arguments, like children, must come at the end of the argument list.
  • we define a custom style for the ul tag using ReactDOMRe.Style. It’s make function takes labeled arguments, followed by the unit operator, which is just an empty set of parens.
  • the “type” attribute on the input tag is written as type_ because “type” is already a keyword in ReasonML.
  • string concatenation must be done using the ++ operator
Note: Piping basically reverses the order of function calls to potentially improve readability. With the |> operator, the items object is applied to each function as the last argument.

Though I prefer the piping style, the following are equivalent.

items 
|> Array.map(videoGameFromJs)
|> Array.map(renderItem)
|> ReasonReact.array;

ReasonReact.array(
Array.map(
renderItem,
Array.map(
videoGameFromJs,
items
)
)
);

I think we are ready to once again compile and serve our project. If you haven’t already, run this command in your project root:

yarn send-introspection-query https://video-game-api-pvibqsoxza.now.sh/graphql

This generates a graphql_schema.json file that Reason Apollo uses to type check your queries. If your ReasonML app asks for a field that isn’t on the schema, or if it doesn’t properly handle optional data types, it will not compile.

The strict typing serves as a wonderful sanity check for writing queries and mutations.

When all is said and done, you should see the following.

Don’t yell at me for not finishing the main story in Skyrim.

Mutating Data

One thing you may notice thus far is that clicking the checkboxes doesn’t do anything. This is expected, since we have not yet wired up a mutation.

Let us begin by recalling our schema above, and creating a module for the mutation to mark a game completed.

Inside VideoGameList.re, add these modules to the top of the file just beneath the call to create a component.

/* VideoGameList.re */
module CompleteGame = [%graphql
{|
mutation CompleteGame($id: ID!) {
completeGame(id: $id) {
id
completed
}
}
|}
];

module CompleteGameMutation = ReasonApollo.CreateMutation(CompleteGame);

For the render prop of the mutation, it’ll look pretty similar to the JavaScript version. I’ll put this code here and then walk through it, starting from inside the <li> tag.

/* VideoGameList.re */

<li key={item.id}>
<CompleteGameMutation>
...{
(mutate, {result}) => {
let loading = result == Loading;
<div>
<input
id={item.id}
type_="checkbox"
checked={item.completed}
onChange={
_event => {
let completeGame =
CompleteGame.make(~id=item.id, ());
mutate(~variables=completeGame##variables, ())
|> ignore;
}
}
/>
<label
htmlFor={item.id}
style={
ReactDOMRe.Style.make(
~color=loading ? "orange" : "default",
(),
)
}>
{item.title ++ " | " ++ item.developer |> str}
</label>
</div>;
}
}
</CompleteGameMutation>
</li>

Like the Apollo VideoGamesQuery component we used earlier, the CompleteGameMutation component we see here passes to its children a mutate function as well as a results object.

This particular component is not the best example to show off how you could use that results object, as I only take advantage of it when a single item is being updated. If it is, I color the text of the item label green and call that the loading state.

I’m no UX guy, but I think that’ll do for today.

Wrapping Up

ReasonML is a pretty powerful and expressive language. If you are new to ReasonML and itching to build some type safe user interfaces, here are some resources to learn from:

  1. This talk from ReasonConf by Sean Grove explains how GraphQL is the ultimate choice for handling external data. Many of the problems with parsing JSON in typed languages are completely averted by using GraphQL.
  2. Many of the third party tools we use in JavaScript come out of the box with ReasonML. This article by David Kopal explains how, along with some other reasons why writing ReasonML is so cool.
  3. Jared Forsyth’s blog has great content about ReasonML and OCaml. He is one of the most active contributors to the community.
  4. I get most of my learning done through the ReasonML docs and the BuckleScript docs. They are easy to follow and contain powerful insights on the design choices when implementing language features.

If you want to quickly set up your own GraphQL server then check out my other article, Learn to Build a GraphQL Server with Minimal Effort.

I hope to write more articles about ReasonML and GraphQL in the future. If these interest you then by all means follow me on Medium and on Twitter!