Turn your Ruby on Rails REST API to GraphQL using Hasura Actions

21 August, 2020

This post is a part of a series which goes over how to convert an existing REST API to GraphQL by defining types and configuring endpoints.

Hasura gives you instant GraphQL CRUD for databases (currently Postgres) which should cover most of the data fetching and real-time subscription use cases. However, sometimes we want to add custom business logic to our API. There are different ways of doing this with Hasura:

Remote schemas: If you have an existing GraphQL server or you're comfortable writing one, you can add this GraphQL server as a Remote schema and Hasura merges it automatically.

Actions: If you'd like to write a new or keep your existing REST API for custom business logic, Hasura actions are the way to go. Actions are a way to extend Hasura’s schema with custom business logic using custom queries and mutations. Actions can be added to Hasura to handle various use cases such as data validation, data enrichment from external sources and any other complex business logic.

In this post, we will focus on Hasura actions and we'll see how to use them to convert a Ruby on Rails REST API to GraphQL.

Create a Hasura Cloud Project

Before we begin, let's create a project on Hasura Cloud to set up the action. Click on the Deploy to Hasura button below, sign up for free and create a new project.

Hasura requires a Postgres database to start with. We can make use of Heroku's free Postgres database tier for the purpose of this app.

Hasura Cloud Database Setup

After signing in with Heroku, you should see the button Create project.

Heroku Database Deployment on Hasura Cloud
Heroku Database Provisioning via Hasura Cloud

Once the project creation is done, click on the Launch the Console button on the Projects tab for the newly created project and the Hasura Console should be visible. By creating this project, you have already got a GraphQL endpoint for the Postgres database that is globally available, secure and scalable from the beginning.

Create an action

Now that we have our Hasura project up and running, let's create an action by heading to the Actions tab on the Hasura console and then clicking the Create button.

Our action will look as follows:

Action Definition

type Mutation {
  registerUser (
    name: String!
    email: String!
    password: String!
  ): UserOutput
}

Types Definition

type UserOutput {
  id : uuid!
}

We just defined a new mutation type called registerUser which accepts the name, email and password arguments and returns the id of the user.

We can add the correct handler URL later. For now, we can just click the Create button to create the action.

Let's verify if the custom mutation was generated by heading to the GraphiQL tab and trying it out:

mutation {
  registerUser(name: "Marion", email: "[email protected]", password: "mysecretpassword") {
    id
  }
}

You will get the following http exception:

The reason for the error is that the handler URL has not been configured yet. But this test confirms that the custom mutation was successfully generated based on the types defined to query on the same GraphQL endpoint.

Now let's go ahead and actually define the Ruby on Rails app, so that this mutation will eventually succeed.

Codegen: Auto generate boilerplate code

Back on the Actions tab and on our newly created action, click on the Codegen tab to auto-generate the code for our Ruby on Rails server. Since we are generating the API from scratch, we need the full server setup instead of just the POST handler.

Business logic required

The following business logic will need to be added in the handler for this action:

  • Receive the action arguments name, email and password on request.
  • Convert the plaintext password input into a hashed secure password.
  • Send a mutation to Hasura to save the newly created user with the hashed password.
  • Return the created user object to signal success, or else error.

Run the action

The generated codegen (registerUser.rb) can be copied to your filesystem to make it possible to make the necessary changes to the handler.

begin
  require "bundler/inline"
rescue LoadError => e
  $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
  raise e
end

gemfile(true) do
  source "https://rubygems.org"
  gem 'rails', '~> 6.0.0'
end

require "action_controller/railtie"

class App < Rails::Application
  routes.append do
    post "/registerUser" => "hasura#register_user_handler"
  end

  config.consider_all_requests_local = true # display errors
end

class HasuraController < ActionController::API
  def register_user_handler
    request_data = params[:input]
    puts request_data
    render json: request_data
  end
end

App.initialize!
Rack::Server.new(app: App, Port: 3000).start
registerUser.rb

Once you copy the codegen, you can set up Rails and start running the server:

  1. If you haven't already, install Ruby
  2. Run the Ruby server:
ruby registerUser.rb

This will first install all the Ruby gems and will then start up a server that is running on port 3000.

Ruby development server

With this, we need to update our handler URL for the action so that the HTTP call works. The registerUser endpoint needs to be added and exposed, so that the handler URL will look as follows http://localhost:3000/registerUser.

Since we need a way to communicate with our Ruby server from the Hasura Cloud instance, we need to expose the Ruby server on a public URL. We can host this server on a cloud instance and point to that URL/IP. But for brevity of this tutorial, we can use ngrok, which gives public URLs for exposing local servers, so that they can be contacted by a cloud instance.

Once you have ngrok installed, in a different tab, you can run the following command:

ngrok http 3000

This should expose your local Ruby server running on port 3000 on a public URL (something like http://b4318819ea61.ngrok.io).

Go back to the Modify tab of the registerUser action that we created earlier on the Hasura Console. Update the handler URL to the above one.

It would look like http://b4318818ea61.ngrok.io/registerUser. Note the usage of registerUser since that's the endpoint we are handling the mutation in.

Test the new endpoint

Finally let's try accessing the /registerUser endpoint through GraphiQL. We want to make the connection work by returning a dummy value for id.

First, we need to make some changes to the handler in registerUser.rb. Because we're using ngrok, we need to add the following line to the Rails application config:

config.hosts = nil

It will then look like this:

class App < Rails::Application
  routes.append do
    post "/registerUser" => "hasura#register_user_handler"
  end
  config.hosts = nil
  config.consider_all_requests_local = true # display errors
end

Also, make the handler endpoint return a dummy value for id:

class HasuraController < ActionController::API
  def register_user_handler
    request_data = params[:input]
    puts request_data
    render json: {id: 1}
  end
end

After that, restart the Rails server.

If we now go back to GraphiQL on the Hasura console, and run the mutation again, the dummy id will be returned:

registerUser mutation served by Ruby

Alright! We have a working GraphQL API that is resolved using a Ruby REST API written using Rails.

Now you can modify your handler code as required to do any business logic; like connecting to a different API, connecting to a database (preferably using Hasura's APIs) or using an ORM for a different databases etc. Hasura will take care of proxying the GraphQL mutation to the right REST API handler internally.

Connecting to database from inside Rails

You can connect to the database and perform any operations. The easier way to perform reads and writes to the database is to use the auto-generated GraphQL API of Hasura inside the handler. By making use of an admin secret you can perform any query from the backend as you will be simulating an admin permission with no restrictions.

Permissions & relationships

The access control logic for this GraphQL API can be declaratively configured using Hasura's role based permission system.

Head over to the Permissions tab on the action page to add a role and allow access.

In the above example, we created a role called public that is allowed to execute this mutation. To read more about how authentication & authorization works with Hasura, you can check out the docs.

This permission can be applied to both custom queries and mutations. Let's look at how this will apply for a relationship. Now consider that the Postgres database has a users table. We can connect the id of the registerUser output to the id of the users table.

Relationship from action to postgres

The related data also conforms to the permissions defined for the respective table (i.e. users).

Existing REST API

So far, what we have been looking at is creating a GraphQL API from scratch by mapping it to a new REST endpoint. What if there is an existing REST API endpoint that you want to reuse as a GraphQL API?

It's quite possible to do this, as long as you are able to handle the request body format that Hasura sends to the endpoint. The format looks something like this:

{
  "action": {
    "name": "<action-name>"
  },
  "input": {
    "arg1": "<value>",
    "arg2": "<value>"
  },
  "session_variables": {
    "x-hasura-user-id": "<session-user-id>",
    "x-hasura-role": "<session-user-role>"
  }
}

In our example, the input arguments - name, email and password - were wrapped inside an input object. Now if you can make necessary modifications to your existing REST API to handle this request body, your GraphQL API will work as expected :)

Deploying a Ruby API

The Ruby API can be deployed as a serverless endpoint or lambda function if you want to architect your application in the 3factor architecture. It really doesn't matter where you deploy it since at the end of the day, Hasura just needs an HTTP POST endpoint for each action handler.

This way, you can host a bunch of endpoints in the same server or hand off to serverless functions for each modular functionality. The GraphQL type system and metadata lives at the Hasura layer and hence your serverless function or endpoint doesn't need to worry about that part of the logic.

search icon

About Hasura

Hasura allows you to mobilize & federate your organisation’s data by building a powerful, secure & flexible GraphQL API, that can query data in your databases, HTTP services, serverless functions as well as third party APIs.
Like what you read? Join our team! We’re hiring

Marion

Marion

Works at Hasura.

Read More