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.
After signing in with Heroku, you should see the button Create project
.
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: "marion@hasura.io", 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.
Once you copy the codegen, you can set up Rails and start running the server:
- If you haven't already, install Ruby
- 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.
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:
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.
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.
Top comments (0)