Reasonable React — Part 2: GraphQL

Sound, Type-Safe GraphQL Interactions

Brandon Konkle
Ecliptic

--

I’m back with more details about how we’re using ReasonML and GraphQL to rapidly build type-safe GraphQL endpoints! I’ve been searching a long time for a stack that can take advantage of the power of sound, expressive typing while still affording me the flexibility and interoperability of JavaScript. I’ve finally found it in Reason!

Today I’m going to introduce you to my GraphQL client structure. I’m taking advantage of some outstanding Reason tools that are brand new and moving quickly, so be ready for a bit of churn behind the scenes. If you can handle adjusting things slightly when libraries are updated, though, then you should be good to go!

Over time, I’ll be abstracting the boilerplate connecting pieces of this architecture into a framework that is easy to re-use. For now, I’ll walk you through how I’m achieving the workflow in the first place.

Type-safe queries with graphql_ppx

To get started, I pull in the graphql_ppx library to automatically type-check my GraphQL queries based on the server schema. This library includes a nifty send-introspection-query script to pull the schema down from the server via an HTTP call. After following the installation instructions and retrieving my schema from the server, I’m ready to write my first mutation:

The [%graphql {| |}] expression tells the compiler that we want to apply the graphql “ppx rewriter” (OCaml’s terminology for a macro or preprocessor) to the multi-line string enclosed in the {| |} syntax. This transforms our query into a set of OCaml functions that can handle all of the input and response data in a type-safe way.

In my ReasonML code, I can immediately begin working with this module by using CreateEvent.make(), a function automatically written by the preprocessor.

To use it in my plain JavaScript, however, I need to take the step of converting messy, chaotic JS input to more structured OCaml input.

With the code above, I’m first opening and assigning a few values I’ll need to use further down in the file. Then, I establish my input type — the messy JS I intend to act on. My fromJs function takes this messy input and converts it to a format OCaml can more effectively handle.

An Aside on User-Level Operators

The fromJs function uses Option.Infix from BsAbstract to introduce the <#> operator. The best practice is to use user-level operators like these sparingly, but I felt it made the code more readable here once the concept is understood.

One reason they are discouraged is because their lack of discoverability in many other ML-influenced languages. It’s often difficult to understand where an operator is actually coming from in those cases, and it’s impossible to search Google with special characters. With OCaml and its weak polymorphism we have a good breadcrumb trail we can use to track down the definition without too much trouble. We find the <#> operator defined the BsAbstract interfaces, and it refers to a Functor reverse map. Functor mapping “lifts” a function up to work on a higher-order type, so this means I’m taking a function like Js.Json.string that returns a Js.Json.t type, and wrapping it in an Option so that it returns option(Js.Json.t) instead.

Executing the Request with Reason-Apollo

The result of the CreateEvent.make() function is a request object that the reason-apollo library understands. To use it, I pass it to an ApiClient module in my project. I’m using this module rather than the built-in components that reason-apollo provides so that I can simply use it as a Promise anywhere I need it.

You can see the entirety of this module here, but for now this is the part that you need to know about:

The graphql_ppx module provides a tool to parse the received data into a more usable structure in OCaml. I’m using the parseResponse function to add a new property to the JavaScript object that is returned by the ApiClient — “parsed”. This property holds the results of the data after being passed through the parse function.

I have two functions right now — one for queries and one for mutations — but I could probably combine them into one without too much trouble. They use the client instance created further up in the file to send a query or a mutation, and use parseResponse before handing the response back to the calling code. The <#> operator is used once again to lift parseResponse to the Promise type.

Putting EventData to good use

To use the resulting EventData interface that I created above in Reason, I can use the CreateEvent module directly.

The >>= operator is the Monad bind operator, derived from the BsAbstract library. It attaches a new step to the Monadic pipeline being constructed. The setNewEventFromResponse function returns a Promise, so I can bind it to the end of the last step in the pipeline — CreateEvent.make(). This allows me to add a new step in the process that fires after the previous step completes, and takes the result of the previous step as its input. If you’re saying to yourself “Oh! That’s just then!” — you’re totally right. The then function is the bind function for a Promise. Yep, that’s right — a Promise is a Monad!

This looks hard. Why should I do it?

Sound type safety does indeed take some more up-front work than dynamic types do. I’ve definitely noticed a difference in the amount of time it takes me to write a ReasonML feature vs. the amount of time it takes to write a plain JS feature. There’s also a much bigger difference in the time it takes to test and refactor a module after I’ve written it.

With my JavaScript code, it’s almost always broken the first time I test it. I have to follow the trail of debug logging and error messages to identify where it fell down, make a change, and try again. This makes features harder to estimate, because you never know how long the testing and bug fixing process may take. You’re nowhere close to done just because you finished writing a component or a state manager.

With my Reason code, it often just works™️ the first time. When things break down, it’s often easy to see the faulty assumption about data types that is the root cause of the breakdown. Testing frequently goes smoothly and is over quickly even when problems do come up. The only exception to this rule is when testing reveals a core assumption that is false and I end up having to rewrite a lot of code. I would have made the same discovery in JavaScript and had to rewrite the code in the same way, but it usually takes me longer to make those same determinations in a loose, dynamically typed environment.

When I do need to make changes, Reason’s sound completeness in its type checking means it’s much more likely to identify the places my changes impact than something more loose like Flow or TypeScript is.

Thankfully, tools like graphql_ppx make it easier to work in this structure. By introspecting your GraphQL schema directly from the server and generating a lot of the boilerplate type checking and manipulation code, you can let your data speak for itself.

That will wrap things up for this installment. If you’d like to see the full project code, head over to ecliptic/reason-events-web and check it out! It includes the full source, with both the api and the client. The server is using a great tool called postgraphile that automatically generates an interface complete with a GraphQL schema based on introspection over an existing Postgres database.

Coming up Next: State!

This is a dense topic that can go in a lot of places, but next time I’m going to tie it all together with State Management! One of the outstanding features of the ReasonML stack is how easy it is to gradually adopt it. I’ll show you how I handle state management in a mosty-type-safe way, allowing it to work smoothly with Redux while still taking advantage of Variants and Pattern Matching.

In the meantime, we’re looking for new consulting projects at Ecliptic! If your team has a big feature, new product, or rewrite coming up — team up with us at Ecliptic to to make it happen! We can help establish core architecture and best practices, while bringing your team up to speed on the stack and helping them build ownership and velocity!

Contact me at brandon@ecliptic.io if you’ve got questions, or if you’re interested in our consulting services! Thanks for reading!

--

--

Brandon Konkle
Ecliptic

Founder and Lead Developer at @eclipticdev, @reasonml acolyte, supporter of social justice, enthusiastic nerd, loving husband & father.