A Reasonable GraphQL Exploration: Part 2
Handling GraphQL requests with Reason and Knex
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!