DEV Community

Vladimir Novick for Hasura

Posted on • Edited on

Building blog CMS in ReasonML with GraphQL and Serverless using Hasura

This is the first part of blog post series where we will create blog cms using Hasura for GraphQL API, and serverless functions for logic and on the client we will write modern and robust code using ReasonML syntax. Let's get started.

ReasonML intro

First of all, before getting into actual code writing, let's discuss why ReasonML? Even though it's a topic for a stand-alone blog post, I will try to give you a brief overview. ReasonML gives us a fantastic type system powered by Ocaml, but as far as syntax goes, it looks pretty close to Javascript. It was invented by Jordan Walke, the guy who created React and is used in production at Facebook messenger. Recently various companies also adopted Reason and use it in production because of it's really cool paradigm: "If it compiles - it works."
This phrase is a very bold statement, but in fact, because Reason is basically a new syntax of OCaml language, it uses Hindley Milner type system so it can infer types in compile time.

What it means for us as developers?

It means that typically we don't write that many types, if at all, as we write in TypeScript for example and can trust the compiler to infer these types.

Speaking of compilation, Reason can be compiled to OCaml, which in turn can compile to various targets such as binary, ios, android etc, and also we can compile to human-readable JavaScript with the help of Bucklescript compiler. In fact that what we will do in our blog post.

What about npm and all these packages we are used to in JavaScript realm?

In fact, BuckleScript compiler gives us powerful Foreign function interface FFI that lets you use JavaScript packages, global variables, and even raw javascript in your Reason code. The only thing that you need to do is to accurately type them to get the benefits from the type system.

Btw if you want to learn more about ReasonML, I streamed 10h live coding Bootcamp on Youtube, that you can view on my channel

ReasonReact

When using Reason for our frontend development, we will use ReasonReact. There are also some community bindings for VueJs, but mainly, when developing for web we will go with ReasonReact. If you've heard about Reason and ReasonReact in the past, recently ReasonReact got a huge update making it way easier to write, so the syntax of creating Reason components now is not only super slick but looks way better than in JavaScript, which was not the case in the past. Also, with the introduction of hooks, it's way easier to create ReasonReact components and manage your state.

Getting started

In official ReasonReact docs, the advised way to create a new project is to start with bsb init command, but let's face it. You probably want to know how to move from JavaScript and Typescript. So in our example, we will start by creating our project with create-react-app.

We will start by running the following command:

npx create-react-app reason-hasura-demo
Enter fullscreen mode Exit fullscreen mode

It will create our basic React app in JavaScript, which we will now change into ReasonReact.

Installation

If it's the first time you set up ReasonML in your environment, it will be as simple as installing bs-platform.

yarn global add bs-platform
Enter fullscreen mode Exit fullscreen mode

Also, configure your IDE by installing appropriate editor plugin

I use reason-vscode extension for that. I also strongly advise using "editor.formatOnSave": true, vscode setting, because Reason has a tool called refmt which is basically built in Prettier for Reason, so your code will be properly formatted on save.

Adding ReasonML to your project

Now it's time to add ReasonML. We will install bs-platform and reason-react dependencies.

yarn add bs-platform --dev --exact
yarn add reason-react --exact
Enter fullscreen mode Exit fullscreen mode

And get into the configuration. For that create bsconfig.json file with the following configuration:

{
  "name": "hasura-reason-demo-app",
  "reason": { "react-jsx": 3 },
  "bsc-flags": ["-bs-super-errors"],
  "sources": [
    {
      "dir": "src",
      "subdirs": true
    }
  ],
  "package-specs": [
    {
      "module": "es6",
      "in-source": true
    }
  ],
  "suffix": ".js",
  "namespace": true,
  "bs-dependencies": [
    "reason-react"
  ],
  "ppx-flags": [],
  "refmt": 3
}
Enter fullscreen mode Exit fullscreen mode

Let's also add compilation and watch scripts to our package.json

"re:build": "bsb -make-world -clean-world",
"re:watch": "bsb -make-world -clean-world -w",
Enter fullscreen mode Exit fullscreen mode

If you run these scripts, what will basically happen is all .re files in your project will be compiled to javascript alongside your .re files.

Start configuring our root endpoint

Let's write our first reason file, by changing index.js from

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Enter fullscreen mode Exit fullscreen mode

to

Basically what I am doing here is render my App component into the dom with

And with

I import register and unregister methods from serviceWorker.js file so I can use Javascript in Reason.

to run our project, we need to run

npm run re:watch
Enter fullscreen mode Exit fullscreen mode

so our Bucklescript will build files for the first time and watch for changes whenever new files are added.

and in different tab let's just run npm start and see our React app.

Basic styling

Styling with ReasonML can be either typed due to bs-css which is based on emotion or untyped. For simplicity, we will use untyped. Let's delete index.css and App.css we have from 'create-react-app', create styles.css file and import two packages:

yarn add animate.css
yarn add tailwind --dev
Enter fullscreen mode Exit fullscreen mode

now in our styles.css file, we will import tailwind

@tailwind base;

@tailwind components;

@tailwind utilities;

Enter fullscreen mode Exit fullscreen mode

and add styles build script in package.json

"rebuild-styles": "npx tailwind build ./src/styles.css -o ./src/index.css",
Enter fullscreen mode Exit fullscreen mode

Writing our first component.

Let's rename our App.css file to App.re, delete all its contents, and write simple ReasonReact component.

Nice right? With ReasonML, we don't need to import or export packages, and in fact, each file is a module, so if our file name is App.re, we can simply use component in a different file.

String to element

In ReasonReact, if you want to add text in component, you do it by using ReasonReact.string

Also, I prefer the following syntax:

You will see it quite a lot in this project. This syntax is reverse-application operator or pipe operator that will give you an ability to chain functions so f(x) is basically written as x |> f.

Now you might say, but wait a second that will be a tedious thing to do in ReasonReact. every string needs to be wrapped with ReasonReact.string. There are various approaches to that.

A common approach is to create utils.re file somewhere with something like

let ste = ReasonReact.string and it will shorten our code to

Through the project, I use ReasonReact.string with a pipe so the code will be more self-descriptive.

What we will be creating

So now when we have our ReasonReact app, it's time to see what we will be creating in this section:

This app will be a simple blog, which will use GraphQL API, auto-generated by Hasura, will use subscriptions and ReasonReact.

Separate app to components

We will separate apps to components such as Header, PostsList, Post AddPostsForm and Modal.

Header

Header will be used for top navigation bar as well as for rendering "Add New Post" button on the top right corner, and when clicking on it, it will open a Modal window with our AddPostsForm. Header will get openModal and isModalOpened props and will be just a presentational component.

We will also use javascript require to embed an SVG logo in the header.

Header button will stop propagation when clicked using ReactEvent.Synthetic ReasonReact wrapper for React synthetic events and will call openModal prop passed as labeled argument (all props are passed as labeled arguments in ReasonReact).

Modal

Modal component will also be a simple and presentational component

For modal functionality in our App.re file, we will use useReducer React hook wrapped by Reason like so:

Notice that our useReducer uses pattern matching to pattern match on action variant. If we will, for example, forget Close action, the project won't compile and give us an error in the editor.

PostsList, Post

Both PostsList and Post will be just presentational components with dummy data.

AddPostForm

Here we will use React setState hook to make our form controlled. That will be also pretty straightforward:

onChange event will look a bit different in Reason but that mostly because of it's type safe nature:

<input onChange={e => e->ReactEvent.Form.target##value |> setCoverImage
}/>
Enter fullscreen mode Exit fullscreen mode

Adding GraphQL Backend using Hasura

Now it's time to set GraphQL backend for our ReasonReact app. We will do that with Hasura.

In a nutshell, Hasura auto-generates GraphQL API on top of new or existing Postgres database. You can read more about Hasura in the following blog post blog post or follow Hasura on Youtube [channel](https://www.youtube.com/c/hasurahq.

We will head to hasura.io and click on Docker image to go to the doc section explaining how to set Hasura up on docker.

We will also install Hasura cli and run hasura init to create a folder with migrations for everything that we do in the console.

Once we have Hasura console running, let's set up our posts table:

and users table:

We will need to connect our posts and users by going back to posts table -> Modify and set a Foreign key to users table:

We will also need to set relationships between posts and users so user object will appear in auto-generated GraphQL API.

Let's head to the console now and create first dummy user:

mutation {
  insert_users(objects: {id: "first-user-with-dummy-id", name: "Test user"}) {
    affected_rows
  }
}

Enter fullscreen mode Exit fullscreen mode

Let's now try to insert a new post:


mutation {
  insert_posts(objects: {user_id: "first-user-with-dummy-id", title: "New Post", content: "Lorem ipsum - test post", cover_img: "https://images.unsplash.com/photo-1555397430-57791c75748a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"}) {
    affected_rows
  }
}

Enter fullscreen mode Exit fullscreen mode

If we query our posts now will get all the data that we need for our client:

query getPosts{
  posts {
    title
    cover_img
    content
    created_at
    user {
      name
      avatar_url
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Adding GraphQL to our app

Let's install a bunch of dependencies to add GraphQL to our ReasonReact app and start getting blog posts in real-time.

yarn add @glennsl/bs-json apollo-boost apollo-link-ws graphql react-apollo reason-apollo subscriptions-transport-ws
Enter fullscreen mode Exit fullscreen mode

When we work with Reason, we want to run an introspection query to our endpoint so we will get our graphql schema introspection data as json. It will be used to give us graphql queries completion and type checking in the editor later on, which is pretty cool and best experience ever.

yarn send-introspection-query http://localhost:8080/v1/graphql    
Enter fullscreen mode Exit fullscreen mode

We also need to add bs-dependencies to our bsconfig.json

"bs-dependencies": [
    "reason-react",
    "reason-apollo",
    "@glennsl/bs-json"
  ],
  "ppx-flags": ["graphql_ppx/ppx"]

Enter fullscreen mode Exit fullscreen mode

We've added graphql_ppx ppx flag here - that will allow us to write GraphQL syntax in ReasonML later on.

Now let's create a new ApolloClient.re file and set our basic ApolloClient

Adding queries and mutations

Queries

Let's head to our PostsList.re component and add the same query we ran previously in Hasura graphiql:

Now we can use GetPostsQuery component with render prop to load our posts. But before that, I want to receive my GraphQL API result typed, so I want to convert it to Records.

It's as simple as adding types in PostTypes.re file

and opening them in any file that will use them open PostTypes

The final version of PostsList component will look as following:

Mutations

To add mutation to our AddPostForm, we start in the same way as with queries:

The change will be in the render prop. We will use the following function to create variables object:

let addNewPostMutation = PostMutation.make(~title, ~content, ~sanitize, ~coverImg, ());
Enter fullscreen mode Exit fullscreen mode

to execute mutation itself, we simply need to run

mutation(
  ~variables=addNewPostMutation##variables,
  ~refetchQueries=[|"getPosts"|],
  (),
) |> ignore;
Enter fullscreen mode Exit fullscreen mode

The final code will look like this:

Adding Subscriptions

To add subscriptions we will need to make changes to our ApolloClient.re. Remember we don't need to import anything in Reason, so we simply start writing.

Let's add webSocketLink

and create a link function that will use ApolloLinks.split to target WebSockets, when we will use subscriptions or httpLink if we will use queries and mutations. The final ApolloClient version will look like this:

Now to change from query to subscription, we need to change word query to subscription in graphql syntax and use ReasonApollo.CreateSubscription instead of ReasonApollo.CreateQuery

Summary and what's next

In this blog post, we've created a real-time client and backend using Hasura, but we haven't talked about Serverless yet. Serverless business logic is something we will look into in the next blog post. Meanwhile, enjoy the read and start using ReasonML.

You can check out the code here:
https://github.com/vnovick/reason-demo-apps/tree/master/reason-hasura-demo and follow me on Twitter @VladimirNovick for updates.

Top comments (7)

Collapse
 
idkjs profile image
Alain

Looks like you don't have the user field set up on posts type. You have user_id so how would you return the name and avatar from the users table?

this query doesnt work if i have the same set up as you:

query getPosts {
  posts {
    title
    cover_img
    content
    created_at
    user {
      name
      avatar_url
    }
  }
}

Collapse
 
idkjs profile image
Alain

Solution is tracking the foreign keys on your tables. See:docs.hasura.io/1.0/graphql/manual/...

Collapse
 
vladimirnovick profile image
Vladimir Novick

Not exactly. an easier solution is what I wrote. You need to add relationships in relationships tab. I added a screenshot to clarify

Collapse
 
fakenickels profile image
Gabriel Rubens Abreu

Nice article!

One small thing though about this part

But before that, I want to receive my GraphQL API result typed, so I want to convert it to Records.

The result already comes typed but as a bound object ({ . "etc": int}) so saying "to receive the result typed" can be a bit confusing for beginners IMO

Collapse
 
vladimirnovick profile image
Vladimir Novick

I would always prefer Reason over typescript because you actually don’t need to type lots of things because compiler will infer types, but you have to be aware of the fact that Reason only looks like JavaScript. It’s way more powerful but you need to understand functional programming constructs such as pattern matching, variants and more. I suggest checking my YouTube channel for 10h ReasonML bootcamp that will cover the basics and even some advanced parts of ReasonML. And soon there will be more content on ReasonReact so stay tuned.

Collapse
 
syntakker profile image
syntakker

It's "Hindley Milner", not "Hindler Miller" type system.

 
vladimirnovick profile image
Vladimir Novick

It was 4 days online bootcamp 3+ h every day.