How to Build a Server with Hanami and CockroachDB | Ruby
Tags: ruby, cockroachdb, hanami
Published: 2023-06-20 17:55:01

---
title: How to Build a Server with Hanami and CockroachDB | Ruby
published: true
date: 2023-06-13 13:24:52 UTC
tags: #ruby #cockroachdb #hanami
canonical_url: https://carlosmv.hashnode.dev/how-to-build-a-server-with-hanami-and-cockroachdb-ruby
---

In this article, we are going to create a CRUD application using Hanami and using Cockroach DB as a database.

We will explore how to create controllers and routes to perform CRUD operations in Hanami and how to test the response of the endpoints using Rspec.

### Hanami

Hanami is a Ruby framework designed to create software that is well-architected, maintainable and a pleasure to work on.

Hanami is a full-stack Ruby web framework. It's made up of smaller, single-purpose libraries.

This repository is for the full-stack framework, which provides the glue that ties all the parts together:

- [**Hanami::Router**](https://github.com/hanami/router) - Rack-compatible HTTP router for Ruby

- [**Hanami::Controller**](https://github.com/hanami/controller) - Full-featured, fast and testable actions for Rack

- [**Hanami::View**](https://github.com/hanami/view) - Presentation with a separation between views and templates

- [**Hanami::Helpers**](https://github.com/hanami/helpers) - View helpers for Ruby applications

- [**Hanami::Mailer**](https://github.com/hanami/mailer) - Mail for Ruby applications

- [**Hanami::Assets**](https://github.com/hanami/assets) - Assets management for Ruby

These components are designed to be used independently or together in a Hanami application.

### Requirements

- Ruby installed

### Installing Hanami

**Hanami** supports Ruby (MRI) 3.0+

PowerShell

```
gem install hanami

```

### Creating a Hanami project

PowerShell

```
hanami new hanami-cockroachdb

```

### Project structure

PowerShell

```
cd hanami-cockroachdb
tree .
.
 Gemfile
 Gemfile.lock
 Guardfile
 README.md
 Rakefile
 app
    action.rb
    actions
 config
    app.rb
    puma.rb
    routes.rb
    settings.rb
 config.ru
 lib
    hanami_cockroachdb
       types.rb
    tasks
 spec
     requests
        root_spec.rb
     spec_helper.rb
     support
         requests.rb
         rspec.rb

9 directories, 16 files


bundle
bundle exec hanami server

```

If you are using a Windows machine, is possible to receive the following message in your command line:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1685821882289/33a1fc10-85dd-4b79-93ed-2e3fd8b0fc46.png)

To solve this issue, we have to open the project with a code editor and make some changes in the `gemfile` and `config/puma.rb` file.

**gemfile**

In the gemfile we have to add the line `gem 'wdm', '>= 0.1.0' if` [`Gem.win`](http://Gem.win)`_platform?`

```
# frozen_string_literal: true

source "https://rubygems.org"

gem "hanami", "~> 2.0"
gem "hanami-router", "~> 2.0"
gem "hanami-controller", "~> 2.0"
gem "hanami-validations", "~> 2.0"

gem "dry-types", "~> 1.0", ">= 1.6.1"
gem "puma"
gem "rake"
#Here we add the line:
gem 'wdm', '>= 0.1.0' if Gem.win_platform?

group :development, :test do
  gem "dotenv"
end

group :cli, :development do
  gem "hanami-reloader"
end

group :cli, :development, :test do
  gem "hanami-rspec"
end

group :development do
  gem "guard-puma", "~> 0.8"
end

group :test do
  gem "rack-test"
end

```

Then, we go to `config/puma.rb`.

**puma. rb**

In this file, we have to comment on the line `workers ENV.fetch("HANAMI_WEB_CONCURRENCY", 2)`, according to [this issue](https://github.com/MartinStyk/quotes-recommender/issues/4) on Puma's GitHub page.

```
# frozen_string_literal: true

max_threads_count = ENV.fetch("HANAMI_MAX_THREADS", 5)
min_threads_count = ENV.fetch("HANAMI_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count

port ENV.fetch("HANAMI_PORT", 2300)
environment ENV.fetch("HANAMI_ENV", "development")
# workers ENV.fetch("HANAMI_WEB_CONCURRENCY", 2)

on_worker_boot do
  Hanami.shutdown
end

preload_app!

```

Now, we try again.

```
bundle
bundle exec hanami server

```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683304966828/5576a29e-0c08-4c32-8a15-cc86b9bbc610.png)

We go to [localhost:2300](http://localhost:2300) in our browser.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683304991625/c81d44d6-7308-472d-b6f7-052487c9e192.png)

### Building the app

Now, let's go to `config/routes.rb` .

```
module HanamiCockroachdb
  class Routes < Hanami::Routes
    root { "Hello from Hanami" }
  end
end

```

As the documentation says, Hanami provides a fast, simple [**router**](https://github.com/hanami/router) for handling HTTP requests.

Your applications routes are defined within the `Routes` class in `config/routes.rb`

Each route in Hanamis router is comprised of:

- An HTTP method (i.e. `get`, `post`, `put`, `patch`, `delete`, `options` or `trace`).

- A path.

- An endpoint to be invoked.

Endpoints are usually actions within your application, but they can also be a block, a [**Rack**](https://github.com/rack/rack) application, or anything that responds to `#call`.

```
# Invokes the FeedReader::Actions:Feeds::Index action
get "/tasks", to: "tasks.index" 

# Invokes the FeedReader::Actions:Feeds::Create action 
post "/tasks", to: "tasks.create"
get "/rack-app", to: RackApp.new
get "/my-lambda", to: ->(env) { [200, {}, ["A Rack compatible response"]] }

```

To add a full set of routes for viewing and managing books, you can either manually add the required routes to your `config/routes.rb` file, or use Hanamis action generator, which will generate actions in addition to adding routes for you.

```
$ bundle exec hanami generate action tasks.index
$ bundle exec hanami generate action tasks.show
$ bundle exec hanami generate action tasks.new
$ bundle exec hanami generate action tasks.create
$ bundle exec hanami generate action tasks.update
$ bundle exec hanami generate action tasks.destroy

```

A `root` method allows you to define a root route for handling `GET` requests to `"/"`. In a newly generated application, the root path calls a block that returns Hello from Hanami. You can instead choose to invoke an action by specifying `root to: "my_action"`. For example, with the following configuration, the router will invoke the `home` action:

```
module HanamiCockroachdb
  class Routes < Hanami::Routes
    root to: "home"
  end
end

```

### Displaying all the tasks

Let's create a route and a controller to display all the entries in an RSS URL.

```
bundle exec hanami generate action tasks.index

```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1685824016263/c6f48183-7fce-4791-ac28-6fd9b1b9224f.png)

Now, we go to the `config/route.rb file`, and we should see that a route for our index action was added.

```
module HanamiCockroachdb
  class Routes < Hanami::Routes
    root { "Hello from Hanami" }
    get "/tasks", to: "tasks.index"
  end
end

```

The `HanamiCockroachdb` module contains the `Routes` class, which inherits from `Hanami::Routes`. The `root` method defines the root route of the web application with a welcome message. The `get` method defines a route for HTTP GET requests to the `/tasks` URL, which maps to the `index` action in the `Tasks` controller.

Now, we go to `app/actions/tasks/index.rb` to code our first controller.

```
# frozen_string_literal: true

module HanamiCockroachdb
  module Actions
    module Tasks
      class Index < HanamiCockroachdb::Action

        def my_task 
          task = {
            "task":"Writing a new article",
            "completed": "false",
          }
          return task
        end

        def handle(*, response)
          task = my_task
          response.format = :json
          response.body = task.to_json
        end
      end
    end
  end
end

```

If use of browser and go to `localhost:2300/tasks`, we should receive the following response in our window:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1685824750420/6b14b048-c390-44b4-8ba8-47fd16f7f1f7.png)

### Testing endpoints with Rspec

We create a `spec/requests/index_spec.rb` file to test the index action.

```
RSpec.describe "GET /tasks", type: :request do
    it "is successful" do
      get "/tasks"

      expect(last_response).to be_successful
      expect(last_response.content_type).to eq("application/json; charset=utf-8")

      response_body = JSON.parse(last_response.body)  
      # Find me in `config/routes.rb`

      expect(response_body).to eq({})
    end
  end


bundle exec rspec spec/requests/index_spec.rb

```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1685841213841/ffb425d7-f4de-4f35-9bb1-3b2d1f6a6c16.png)

```
RSpec.describe "GET /tasks", type: :request do
    it "is successful" do
      get "/tasks"

      expect(last_response).to be_successful
      expect(last_response.content_type).to eq("application/json; charset=utf-8")

      response_body = JSON.parse(last_response.body)  

      expect(response_body).to eq({"task"=>"Writing a new article","completed" =>"false"})
    end
  end

```

We run again the `bundle exec rspec spec/requests/index_spec.rb` command in our terminal. And we should see the following response:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1685841465805/99bbe2d8-8e43-4534-9b92-8a22a2abe95e.png)

### Creating a CockroachDB Cluster

We have to have a CockroachDB account and create a cluster. You can [sign in here](https://cockroachlabs.cloud/).

After we create a cluster. We press on `Connect`.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677960798925/cb7d940a-418c-4870-a8f5-c3ab132a3599.png?auto=compress,format&format=webp)

Then, we select Ruby in the language field and Pg in the tool field.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1685842396652/9135c3da-b03c-4ff3-b9f3-cc9fc000f9a1.png)

We copy the `DATABASE_URL`, which is the connection URL. Through this URL we connect our app to the cluster.

When we create a new cluster, we have to download a CA certificate. CockroachDB shows us a URL we have to copy and paste into our command line or PowerShell on Windows, to download the certificate.

The URL has the form:

```
mkdir -p $env:appdata\postgresql\; Invoke-WebRequest -Uri https://cockroachlabs.cl

```

We need to add these dependencies to the gemfile and run `bundle install`:

```
# Gemfile
gem "rom", "~> 5.3"
gem "rom-sql", "~> 3.6"
gem "pg"

group :test do
  gem "database_cleaner-sequel"
end

```

We run `bundle install` in our command line.

According to the [documentation](https://guides.hanamirb.org/v2.0/introduction/getting-started/#persisting-books), in Hanami, [providers](https://guides.hanamirb.org/v2.0/app/providers/) offer a mechanism for configuring and using dependencies, like databases, within your application.

Copy and paste the following provider into a new file at `config/providers/persistence.rb`:

```
Hanami.app.register_provider :persistence, namespace: true do
    prepare do
      require "rom"

      config = ROM::Configuration.new(:sql, target["settings"].database_url)

      register "config", config
      register "db", config.gateways[:default].connection
    end

    start do
      config = target["persistence.config"]

      config.auto_registration(
        target.root.join("lib/tasks/persistence"),
        namespace: "HanamiCockroachdb::Persistence"
      )

      register "rom", ROM.container(config)
    end
  end

```

For this persistence provider to function, we need to establish a `database_url` setting.

Settings in Hanami are defined by a `Settings` class in `config/settings.rb`:

```
# frozen_string_literal: true

module HanamiCockroachdb
  class Settings < Hanami::Settings
    # Define your app settings here, for example:
    #
    # setting :my_flag, default: false, constructor: Types::Params::Bool
    setting :database_url, constructor: Types::String
  end
end

```

Settings can be strings, booleans, integers and other types. Each setting can be either optional or required (meaning the app wont boot without them), and each can also have a default.

You can read more about Hanamis settings in the [Application guide](https://guides.hanamirb.org/v2.0/app/settings/).

Lets add `database_url` and make it a required setting by using the `Types::String` constructor:

**.env file**

In the `.env file` we paste the DATABASE\_URL of the Cockroach DB cluster.

```
DATABASE_URL=<CockroachDB DATABASE_URL>

```

**Rakefile**

Enable rom-rb's rake tasks for database migrations by appending the following code to the Rakefile:

```
# frozen_string_literal: true

require "hanami/rake_tasks"
require "rom/sql/rake_task"

task :environment do
  require_relative "config/app"
  require "hanami/prepare"
end

namespace :db do
  task setup: :environment do
    Hanami.app.prepare(:persistence)
    ROM::SQL::RakeSupport.env = Hanami.app["persistence.config"]
  end
end

```

With persistence ready, we can now create the tasks table.

To create a migration run:

```
$ bundle exec rake db:create_migration[create_tasks]

```

Now, we go to `db/<timestamp>_create_tasks.db` which is the migration file, to define our table.

```
# frozen_string_literal: true

ROM::SQL.migration do
  change do
  end
end

```

Now, we change this file, to create our table.

Initially, I want to store the tasks in the database, for now.

Here, we define the table, with three fields: Primary key, the name of the task, and its status.

```
ROM::SQL.migration do
  change do
    create_table :tasks do
      primary_key :id
      column :task, :text, null: false
      column :completed, :text, null: false
    end  
  end
end

```

Now we run the following command to run migrations.

```
bundle exec rake db:migrate

```

Then, lets add a `rom-rb` relation to allow our application to interact with our tasks table. Create the following file at `lib/hanami_cockroachdb/persistence/relations/tasks.rb`:

```
module HanamiCockroachdb
    module Persistence
      module Relations
        class Tasks < ROM::Relation[:sql]
          schema(:tasks, infer: true)
        end
      end
    end
  end

```

### Create action

Now, we run the following command to create a new action:

```
$ bundle exec hanami generate action tasks.create

```

This new action is to perform POST operation and create a new row in the database.

We have to add the `body_parser` middleware to be able to parse the body of a POST request.

We go to the `config/app.rb` file and add the line `config.middleware.use :body_parser, :json` to the App class.

```
require "hanami/action"

module HanamiCockroachdb
  class App < Hanami::App
    config.middleware.use :body_parser, :json
  end
end

```

With this parser, the `task` key will be available in the action via `request.params[:task]`.

If we go to `config/routes.rb` we can confirm that a route for POST requests was added.

```
# frozen_string_literal: true

module HanamiCockroachdb
  class Routes < Hanami::Routes
    root { "Hello from Hanami" }
    get "/tasks", to: "tasks.index"
    post "/tasks", to: "tasks.create"
  end
end

```

We go to `app/actions/tasks/create.rb` to write the handler for POST requests.

```
# frozen_string_literal: true

module HanamiCockroachdb
  module Actions
    module Tasks
      class Create < HanamiCockroachdb::Action
        include Deps["persistence.rom"]

        params do
          required(:task).hash do
            required(:task).filled(:string)
            required(:completed).filled(:string)
          end
        end 

        def handle(request, response)
          if request.params.valid?
            task = rom.relations[:tasks].changeset(:create, request.params[:task]).commit

            response.status = 201
            response.body = task.to_json
          else
            response.status = 422
            response.format = :json
            response.body = request.params.errors.to_json
          end   
        end
      end
    end
  end
end

```

This class extends the `HanamiCockroachdb::Action` class and includes a dependency on `persistence.rom`. In this class, we define a `params` block that requires two fields, `task` and `completed`, both of which must be of type `string`.

Then we define the `handle` method that takes in a request and a response object. Within the `handle` method, we first check if the parameters in the request are valid by checking `request.params.valid?`. If they are, we create a new `task` in the database using a changeset from `rom.relations[:tasks]`, passing in the `task` parameter value from the request as an argument.

If the task creation is successful, we set the response status to 201 (created) and return the task object as JSON in the response body. If the parameters are not valid, we set the response status to 422 (unprocessable entity) and return the validation errors in JSON format as the response body.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1685927323389/21be5041-2c38-4242-91f5-bc625cf72e16.png)

We can write a test for the POST request.

We create `spec/requests/create_spec.rb` file to test the create controller.

```
# spec/requests/create_spec.rb

RSpec.describe "POST /tasks", type: [:request, :database] do
    let(:request_headers) do
      {"HTTP_ACCEPT" => "application/json", "CONTENT_TYPE" => "application/json"}
    end

    context "given valid params" do
      let(:params) do
        {task: {task: "Buy the groceries", completed: "true"}}
      end

      it "creates a task" do
        post "/tasks", params.to_json, request_headers

        expect(last_response).to be_created
      end
    end

    context "given invalid params" do
      let(:params) do
        {task: {task: nil}}
      end

      it "returns 422 unprocessable" do
        post "/tasks", params.to_json, request_headers

        expect(last_response).to be_unprocessable
      end
    end
  end

```

We run the `bundle exec rspec spec/requests/create_spec.rb` command, to run the test.

If everything is good. We should see the following message:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1686150952823/7e87da2c-f172-4980-b1aa-a06630a19a17.png)

### Index action

There is no need to create another action, we just need to rewrite the code so it will be able to make queries to the database.

```
# frozen_string_literal: true

module HanamiCockroachdb
  module Actions
    module Tasks
      class Index < HanamiCockroachdb::Action
        include Deps["persistence.rom"]

        def handle(*, response)
          task = rom.relations[:tasks]
            .select(:id, :task, :completed)
            .to_a
          response.format = :json
          response.body = task.to_json
        end
      end
    end
  end
end

```

Within the `handle` method, we first access the `tasks` relation from `rom.relations[:tasks]`. We then select the `id`, `task`, and `completed` columns from the `tasks` table. Finally, we convert the returned tasks to an array and set the response format to JSON and body to the array of tasks as JSON.

### Show action

To create a show action to retrieve a task by its ID. We run the `bundle exec hanami generate action tasks.show` command in our console.

A new route will be added to the `config/routes.rb` file,

```
module HanamiCockroachdb
  class Routes < Hanami::Routes
    root { "Hello from Hanami" }
    get "/tasks", to: "tasks.index"
    post "/tasks", to: "tasks.create"
    get "/tasks/:id", to: "tasks.show"
  end
end

```

Now, we create a `spec/requests/show_spec.rb` file, to write a test for the `show` action.

```
# spec/requests/show_spec.rb

RSpec.describe "GET /tasks/:id", type: [:request, :database] do
    let(:task) { app["persistence.rom"].relations[:tasks] }

    context "when a task matches the given id" do
      let!(:id) do
        task.insert(task: "Publish a new article", completed: "false")
      end

      it "renders the task" do
        get "/tasks/#{id}"

        expect(last_response).to be_successful
        expect(last_response.content_type).to eq("application/json; charset=utf-8")

        response_body = JSON.parse(last_response.body)

        expect(response_body).to eq(
          "id" => id, "task" => "Publish a new article", "completed" => "false"
        )
      end
    end

    context "when no task matches the given id" do
      it "returns not found" do
        get "/tasks/#{task.max(:id).to_i + 1}"

        expect(last_response).to be_not_found
        expect(last_response.content_type).to eq("application/json; charset=utf-8")

        response_body = JSON.parse(last_response.body)

        expect(response_body).to eq(
          "error" => "not_found"
        )
      end
    end
  end

```

Now, we run test by running the `bundle exec rspec spec/requests/show_spec.rb` command.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1686188239850/e3a98310-78c0-4bd7-996f-4e99bff91b6e.png)

The test fails because we didn't write our handler yet.

```
# frozen_string_literal: true
require "rom"
module HanamiCockroachdb
  module Actions
    module Tasks
      class Show < HanamiCockroachdb::Action
        include Deps["persistence.rom"]

        params do
          required(:id).value(:integer)
        end

        def handle(request, response)

          task = rom.relations[:tasks].by_pk(
            request.params[:id]
          ).one
          response.format = :json

          if task
            response.body = task.to_json

          else
            response.status = 404
            response.body = {error:"not_found"}.to_json

          end  
        end
      end
    end
  end
end

```

This class extends the `HanamiCockroachdb::Action` class and includes a dependency on `persistence.rom`. The class also defines a `params` block for validating the input params and ensuring that only an `id` of a specific type is provided.

Within the `handle` method, we first retrieve the task from the `tasks` relation using the `by_pk` method of `rom.relations[:tasks]` and passing in the `id` parameter from the request. We then format the response as JSON.

If a task is found, we set the response body to the task details in JSON format. If no task is found, we set the response status to `404` (not found) and set the response body to an error message as JSON.

If we run the test again, it should be a success.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1686188907317/9e5a35a2-38ac-402f-a700-7972586c71d5.png)

### Update action

For the update action, we create `spec/requests/update_spec.rb` file to test the update action.

```

RSpec.describe "PATCH /tasks/:id", type: [:request, :database] do
    let(:task) { app["persistence.rom"].relations[:tasks] }
    let!(:id) do
      task.insert(task: "Publish a new article", completed: "false")
    end

    context "when a task matches the given id" do

        it "renders the task" do
          patch "/tasks/#{id}", {"task": {"task":"Publish a new article", "completed":"false"}}.to_json, "CONTENT_TYPE" => "application/json"

          expect(last_response).to be_successful
          expect(last_response.content_type).to eq("application/json; charset=utf-8")

          response_body = JSON.parse(last_response.body)

          expect(response_body).to eq(
            "id" => id, "task" => "Publish a new article", "completed" => "false"
          )
        end
      end

    context "given valid params" do

      it "should update the task" do
        patch "/tasks/#{id}", {"task": {"task":"Update task", "completed":"true"}}.to_json, "CONTENT_TYPE" => "application/json"

        expect(last_response).to be_successful
        updated_task = task.by_pk(id).one

        expect(updated_task[:task]).to eq("Update task")
        expect(updated_task[:completed]).to eq("true")
      end
    end

    context "given invalid params" do

      it "returns 422 unprocessable" do
        patch "/tasks/#{id}", {task: {task: nil}}.to_json, "CONTENT_TYPE" => "application/json"

        expect(last_response).to be_unprocessable
      end
    end

    context "when no task matches the given id" do
      it "returns not found" do
        patch "/tasks/#{task.max(:id).to_i + 1}", {"task": {"task":"Update task", "completed":"true"}}.to_json, "CONTENT_TYPE" => "application/json"

        expect(last_response).to be_not_found

        response_body = JSON.parse(last_response.body)

        expect(response_body).to eq(
          "error" => "not_found"
        )
      end
    end
end

```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1686236306338/144d3a51-5e12-4fcf-adda-161afd017e2a.png)

Let's create the update action by running this command in our console: `bundle exec hanami generate action tasks.update`

It should generate a PATCH route inside the `config/routes.rb` file.

```
# frozen_string_literal: true

module HanamiCockroachdb
  class Routes < Hanami::Routes
    root { "Hello from Hanami" }
    get "/tasks", to: "tasks.index"
    post "/tasks", to: "tasks.create"
    get "/tasks/:id", to: "tasks.show"
    patch "/tasks/:id", to: "tasks.update"
  end
end

```

We go to `app/actions/tasks/update.rb` to write the handler for PATCH requests.

```
# frozen_string_literal: true

module HanamiCockroachdb
  module Actions
    module Tasks
      class Update < HanamiCockroachdb::Action
        include Deps["persistence.rom"]

        params do
          required(:id).value(:integer)
          required(:task).hash do
            required(:task).filled(:string)
            required(:completed).filled(:string)
          end
        end 

        def handle(request, response)
          if request.params.valid?
            task = rom.relations[:tasks].by_pk(
              request.params[:id]
            ).one
            response.format = :json

            if task
              task = rom.relations[:tasks].by_pk(request.params[:id]).changeset(:update, request.params[:task]).commit
              response.body = task.to_json

            else
              response.status = 404
              response.body = {error:"not_found"}.to_json

            end
          else
            response.status = 422
            response.format = :json
            response.body = request.params.errors.to_json
          end       
        end
      end
    end
  end
end

```

In this class, we define a `params` block for validating the input params and ensuring that only an `id` of a specific type is provided.

Like in the Show Action, within the `handle` method, we first retrieve the task from the `tasks` relation using the `by_pk` method of `rom.relations[:tasks]` and passing in the `id` parameter from the request. We then format the response as JSON.

If a task is found, we retrieve the changeset by leveraging the method `changeset` of the relation object `rom.relations[:tasks]` and committing the changes by using the `commit` method. This updates the task in the database and returns the updated task.

If no task is found, we set the response status to `404` (not found) and set the response body to an error message as JSON.

If we run our test again with the `bundle exec rspec spec/requests/update_spec.rb` command, we should receive the following output:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1686538345187/06d782ae-3420-4600-b79b-8f5234eb4575.png)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1686240174487/0d068749-0c42-48c7-851e-a0c2aeed65a9.png)

### Destroy action

The last action we will create is the destroy action, the action that will handle the DELETE request given a specific ID.

We run the following command to create this action: `bundle exec hanami generate action tasks.destroy` .

It will add a DELETE route to the `config/routes.rb`.

```
module HanamiCockroachdb
  class Routes < Hanami::Routes
    root { "Hello from Hanami" }
    get "/tasks", to: "tasks.index"
    post "/tasks", to: "tasks.create"
    get "/tasks/:id", to: "tasks.show"
    patch "/tasks/:id", to: "tasks.update"
    delete "/tasks/:id", to: "tasks.destroy"
  end
end

```

Now, we go to `app/actions/tasks/destroy.rb`.

```
# frozen_string_literal: true

module HanamiCockroachdb
  module Actions
    module Tasks
      class Destroy < HanamiCockroachdb::Action
        include Deps["persistence.rom"]
        params do
          required(:id).value(:integer)

        end 

        def handle(request, response)
          task = rom.relations[:tasks].by_pk(
            request.params[:id]
          ).one

          if task
            task = rom.relations[:tasks].by_pk(request.params[:id]).command(:delete)
            task.call
            response.body = "Task Deleted"

          else
            response.status = 404
            response.body = {error:"not_found"}.to_json

          end  
        end
      end
    end
  end
end

```

We define a `params` block for validating the input params and ensuring that only an `id` of a specific type is provided.

Within the `handle` method, we first retrieve the task from the `tasks` relation using the `by_pk` method of `rom.relations[:tasks]` and passing in the `id` parameter from the request.

If the task is found, we retrieve the command to delete the task by using the `command` method of the relation object `rom.relations[:tasks]` and passing in the `:delete` argument. We then execute the command by calling the `call` method on the `task` object, effectively deleting the task from the database.

If no task is found, we set the response status to `404` (not found) and set the response body to an error message as JSON.

Now, lets test this endpoint. We create a new file, `spec/requests/destroy_spec.rb` for testing the destroy action.

```
# spec/requests/delete_spec.rb

RSpec.describe "DELETE /tasks/:id", type: [:request, :database] do
    let(:task) { app["persistence.rom"].relations[:tasks] }

    context "when a task matches the given id" do
      let!(:id) do
        task.insert(task: "Publish a new article", completed: "false")
      end

      it "deletes the task" do
        delete "/tasks/#{id}"

        expect(last_response).to be_successful

        response_body = last_response.body

        expect(response_body).to eq(
          "Task Deleted"
        )
      end
    end

    context "when no task matches the given id" do
      it "returns not found" do
        delete "/tasks/#{task.max(:id).to_i + 1}"

        expect(last_response).to be_not_found

        response_body = JSON.parse(last_response.body)

        expect(response_body).to eq(
          "error" => "not_found"
        )
      end
    end
  end

```

### Conclusion

In conclusion, building a CRUD API with Hanami can be a rewarding experience. Hanami provides a set of tools that allow developers to create web applications quickly and efficiently. Hanami is an excellent choice for building APIs that require high performance and reliability.

During this tutorial, we have seen how to set up a basic CRUD API using Hanami and how to integrate it with a Cockroach database. We started by generating a new Hanami project using the hanami CLI tool and then creating database. We then went on to create RESTful routes for our API endpoints and implemented the basic CRUD operations, including adding, updating, deleting, and reading data records from the database.

In addition to the core features, we also covered Hanami's test features using Rspec and wrote tests for every endpoint. We demonstrated how to use Hanami's validators to ensure that requests and data are validated and sanitized before persisting them in the database.

### Resources

- [Hanami Site](https://hanamirb.org/)

- [Hanami Getting Started](https://guides.hanamirb.org/v2.0/introduction/getting-started/)

- [Hanami Routing Overview](https://guides.hanamirb.org/v2.0/routing/overview/)

- [Rack-cors.](https://github.com/cyu/rack-cors)