Getting Started with Hanami and GraphQL
What is GraphQL?
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, it gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
What is Hanami?
Hanami is a Ruby MVC web framework comprised of many micro-libraries. It has a simple, stable API, a minimal DSL, and prioritises the use of plain objects over magical, over-complicated classes with too much responsibility.
The natural repercussion of using simple objects with clear responsibilities is more boilerplate code. Hanami provides ways to mitigate this extra legwork while maintaining the underlying implementation.
Project setup
If you haven't already done so, install hanami.
gem install hanamiAfter hanami is installed on your machine, you can create a new project. Feel free to chose another database or test framework if you like.
hanami new blogs --database=postgres --application-name=api --test=rspec
cd blogsDefine entities
Before we do anything at all, we need entities we can query over our API. Hanami offers a generator for entities which can be invoked by the following command:
hanami generate model authorThis will generate an entity and the corresponding test. In this tutorial tests are omitted for brevity but you are encouraged to implement them on your own.
We start out with our author as it's a very simple model. It has a single attribute 'name'.
# lib/blogs/entities/author.rb
class Author
  include Hanami::Entity
  attributes :name
endNext we're going to generate another model. A blog.
hanami generate model blogFor our blog, we want a title, content and an author_id to reference the author.
# lib/blogs/entities/blog.rb
class Blog
  include Hanami::Entity
  attributes :title, :content, :author_id
endUpdate database
To be able to store entities, we need to define database tables to hold data. Create a migration for the author model first:
hanami generate migration create_authorsHanami will generate a migration file with a current timestamp for you under db/migrations. Open the file and add the following:
Hanami::Model.migration do
  change do
    create_table :authors do
      primary_key :id
      column :name, String, null: false
    end
  end
endFor blogs, we create another migration named create_blog.
hanami generate migration create_blogsInside the migration create another table with columns for our blog:
Hanami::Model.migration do
  change do
    create_table :blogs do
      primary_key :id
      column :title, String, null: false
      column :content, String, null: false
      foreign_key :author_id, :authors
    end 
  end
endTo get the changes to our database, execute
hanami db create hanami db migrateIn order to be able to run database-backed tests, we need to ensure that the test database uses the same schema as our development database. Update the schema by setting HANAMI_ENV to test explicitly:
HANAMI_ENV=test hanami db create
HANAMI_ENV=test hanami db migrateNow that our database is ready, we can go ahead and define mappings for author and blog. Go to lib/blogs.rb, find the mapping section and add mappings for the new entities.
##
# Database mapping
#
# Intended for specifying application wide mappings.
#
mapping do
  collection :blogs do
    entity Blog
    repository BlogRepository
    attribute :id, Integer
    attribute :title, String
    attribute :content, String
    attribute :author_id, Integer
  end
  collection :authors do
    entity Author
    repository AuthorRepository
    attribute :id, Integer
    attribute :name, String
  end
endIntroducing Types
After having defined our entities, we can now move on to create GraphQL types. First update your Gemfile and add the following line:
gem 'graphql'and then run
bundle installWe're going to place type definitions in a dedicated directory to keep them separate from our entities. Furthermore those types are relevant for our web API only and not for the whole application. Create a directory in apps/api/ named types
mkdir -p apps/api/typesand update your web app's application.rb file to include type definitions in the load path.
load_paths < 'apps/api/types'# apps/api/types/query_type.rb
require_relative 'author_type'
require_relative 'blog_type'
QueryType = GraphQL::ObjectType.define do
  name 'Query'
  description 'The query root for this schema'
  field :blog do
    type BlogType
    argument :id, !types.ID
    resolve -> (_, args, _) {
      BlogRepository.find(args[:id])
    }
  end
  field :author do
    type AuthorType
    argument :id, !types.ID
    resolve -> (_, args, _) {
      AuthorRepository.find(args[:id])
    }
  end
end# apps/api/types/blog_schema.rb
require_relative 'query_type'
BlogSchema = GraphQL::Schema.define(query: QueryType)Notice the require_relative statements at the beginning of some files. This is a workaround because, even though defined in the load path, types don't seem to be auto loaded inside a type definition file.
... and Action
Now that the schema definitions and load path are set up correctly, it is time to create the action that will serve query requests. To generate a new action invoke the following command:
hanami generate action api graphql#show --skip-viewSince we're providing the --skip-view flag Hanami will not generate a view class and template for this action. The above command will generate a new action where we place query logic.
# apps/api/controllers/graphql/show.rb
module Api::Controllers::Graphql
  class Show
    include Api::Action
    def call(params)
      query_variables = params[:vairables] || {}
      self.body = JSON.generate(
        BlogSchema.execute(
          params[:query],
          variables: query_variables
        )
      )
    end
  end
endTo let Hanami know that it shouldn't render a view, we set self.body directly inside the action.
Query the API
In order to see the API working, we need data! Fire up your hanami console and create some author and blogs.
hanami cNow create one or more authors and save it to database via AuthorRepository:
author = Author.new(name: 'John Wayne')
AuthorRepository.persist authorDo the same for Blogs
blog = Blog.new(title: 'first blog', content: 'lorem ipsum dolor sit met', author_id: 1)
BlogRepository.persist blogAs soon as we have our data in place, we can use cURL to query our API.
curl -XGET -d 'query={ blog(id: 1) { title author { name } }}' http://localhost:2300/graphqlIf all goes well you should see a response looking something like this:
{"data":{"blog":{"title":"first blog","author":{"name":"John Wayne"}}}}Go ahead and play around with the query. If you look at the type definition for QueryType you'll notice, that it should be possible to query for authors, too. Can you get the API to list all blog titles for a given author?
That's it. This introduction should give you a glimpse into Hanami and GraphQL. You can find more information in the section below.
Links and references
- More information about GraphQL
- The Hanami homepage
- Github repository for GraphQL-Ruby
- Marc-André Giroux's blogpost this article is based upon
- Source code from this article.