A Reasonable GraphQL Exploration: Part 2

Handling GraphQL requests with Reason and Knex

Brandon Konkle
Ecliptic

--

Update: Part 3 is published!

In my last article, I introduced ReasonML and OCaml and began to build up a module to handle data from a GraphQL schema. This is just the tip of the iceberg, however! Next, I’ll walk you through how to set up a PaperClipService to perform operations for our PaperClip model, and a PaperClipHandler we can use to connect it with GraphQL using Apollo.

Connecting with Knex

The purpose of my PaperClipService is to work with the data source to perform operations on our PaperClips. I’ve been using PostgreSQL for a decade now, and I strongly recommend it for both relational and document-store style persistence. My favorite tool for interacting with it in Node right now is Knex. I prefer it because it’s not an Object-Relational-Mapper, like Sequelize. Knex solves a simpler problem — constructing SQL queries and sending them to the database. It uses a promise-based interface, which is easy to work with in Reason. I’ve published a simple interface for working with Knex, bs-knex.

Database Setup

To get started, I use Knex’s migration tools to run a small migration script:

I create a knexfile.js in the project root, and run knex migrate:latest to establish my database. The details here are outside the scope of this article, but the Knex docs do a great job of getting the reader up to speed.

Defining the Relationship

Next, it’s time to type my interface. This isn’t required, but it has been very helpful to me during development both to prompt me to think things through ahead of time and to validate my service methods as I write them.

As mentioned before, t is the naming convention for the main type of a module. My type above is a Reason Record, not a JavaScript object. To distinguish the two, JS objects use parentheses for the object keys, indicating that they are string-based keys. Type annotations for JS objects include a . or a .. to indicate whether they are “open” or “closed” objects. More information can be found in the BuckleScript docs.

My PaperClipService.t record defines fields for each of the operations I want to be able to carry out on my PaperClip table in the database. Each method takes parameters and returns a promise. One of the first things that may jump out at you is the ~ character in front of the arguments. This is Reason’s labeled argument syntax, which ends up being a fairly powerful feature combined with Reason’s auto-currying and unlabeled arguments. In this case, however, I’m simply using them to make the interface easier to read at a glance using the code intelligence tools in my IDE.

Initializing the Service

Next, I define how the service is initialized. As you’ll see in part 3 of this series, I’ve created an Express architecture that initializes DataProviders prior to launching the server application. These DataProviders initialize tools like Knex so that they are ready to use in the services.

I use the make convention from OCaml, and write a simple function that sets up a Knex query for my service methods to make use of.

My paperClips query starter is passed to each service method as the first argument, making it very easy to test those methods in isolation by passing in a mock query object.

Performing Operations

Now it’s time to write out some implementations! I’ll start with getById because it’s one of the easier ones.

My function takes the paperClips query starter and filters it with a where clause using the id that was passed in. Since Reason auto-curries, the result of the make function above is a record with functions that take the rest of the parameters each service method is looking for, only then executing the method. This means that getById above is a function that accepts ~id and then runs the query, since the initial paperClips argument has already been passed to handleGetById.

I follow up my where clause with a select statement, telling Knex to return everything and let GraphQL sort it out. Then, I transition the chain to a promise to handle what happens after the query is complete.

Handling Responses

Next, I call a couple of utility functions to handle the response. The first one is handleResponse, and this is actually something I define a little further up in the file.

The point of this is merely to set up the KnexUtils.rejectIfEmpty function with the decoder we’ll use to decode responses from JSON. The rejectIfEmpty function relies on Knex’s convention to return results in an array. If that array is empty, then the operation failed without throwing an error. My handleResponse function applies the ~decoder argument to rejectIfEmpty, but leaves the ~error argument for the caller to define.

In handleGetById, I use it like this: |> then_(handleResponse(~error="No PaperClip found with id: " ++ id )). If the response is empty, it will be rejected with my error message.

I’m also using the KnexUtils.pickFirst tool. This pulls the first item out of the array and resolves the promise with it. I’m using it here because I only care about the first item of the array that Knex returns, because there should be only one row retrieved.

Catching Errors

To cap off the promise, I use KnexUtils.handleDbErrors. This catches Knex database errors and replaces them with user-friendly messages. In the bs-knex library, I’m building up a variety of additional error handlers that can be added to the promise chain, like KnexUtils.handleUniqueError.

These work by inspecting the error instance and looking for properties specific to Knex errors, like code and constraint. If none are found, the error is simple re-rejected so that it can continue down the promise chain. If the error matches, then it is replaced with the more user-friendly error passed to it.

Finishing Up

Take a look at the finished product here to see how the rest of the service methods shake out. Now that we’ve got these out of the way, it’s time to hook it up to GraphQL!

Handling GraphQL Requests

Now that we have our service ready to handle requests, it’s time to figure out how to actually give it requests. One minor change to mention from Part 1 of this series — I’ve moved my resolvers out of the PaperClip.re module and into my PaperClipHandler.re module. This broke up the modules a little more evenly, and made more sense given how the resolvers are used. The Resolve module itself is still in the PaperClip.re module, however, because it is used by the Encoder.

Resolving Values

I start out by defining a type for the resolvers, and then implementing the value.

The open PaperClip.Resolve; statement pulls all of the values from my Resolve module into the current scope. This makes it very clean and easy to define my resolvers.

My resolvers type is a nested, closed JavaScript object with the structure that Apollo expects. My resolvers value (which is a separate thing from the resolvers type to the compiler) uses the Resolve module to pull each attribute out of the paperClip instance and return them.

Describing the Interface

Next, I create a t type for my PaperClipHandler module.

My Apollo context is empty right now and I’m not using it, so I just define a placeholder type, graphQLContext, to represent it. I also define a quick type to handle the empty response I want to return from my “remove” operation.

Translating Requests

As you’ll see in Part 3, the resolvers, queries, and mutations are pieced together for Apollo and handed to the Express middleware it includes. Each operation takes input in the JavaScript object format used by Apollo. The point of the Handler is to translate requests in Apollo format to requests that our PaperClipService understands.

My Handler takes in the DataProvider because it needs to initialize the Service with it. It then returns a record matching our type above. Each handler method takes the JavaScript input, and then calls the service with the labeled arguments needed.

As you can see by the allPaperClips implementation, sometimes a little processing is needed to translate the request effectively. This is the whole point of separating the Handler module from the Service module.

Liftoff!

I’ll bet you’re ready to fire up GraphiQL and make some requests, right? Well you’re in luck — Part 3 of my guide will walk you through how to plug this PaperClipHandler into Apollo and Express!

In the meantime, if you’re looking for agile React, React Native, and Node development from an innovative team with a passion for software craftsmanship — contact me! Ecliptic is growing, and we’re always happy to talk about new projects!

--

--

Brandon Konkle
Ecliptic

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