Simplifying Backend APIs with Macaw Framework: A CRUD Tutorial
Tags: ruby, webdev, backend, programming
Published: 2023-05-05 12:16:08

If you're tired of configuring a lot of parameters and things you wouldn't even use when creating simple backend APIs, you might want to check out Macaw Framework. In my previous post, I introduced [Macaw Framework](https://dev.to/ariasdiniz/introducing-the-macaw-framework-a-simple-evolving-ruby-web-framework-lb3), a lightweight and flexible web framework. Now, I'd like to show you how to use Macaw Framework to build a simple CRUD application.

It's worth noting that, despite being production ready, Macaw Framework is still in its first beta version. I encourage you to try it out, give feedback, and report any issues you encounter. Your feedback will be invaluable in helping me to improve the framework and make it even better for the community.

In this article, I'll guide you through the steps to build a simple CRUD application using Macaw Framework. You can find the code for this application on GitHub [here](https://github.com/ariasdiniz/macaw_crud_example). If you're not familiar with Macaw Framework, I encourage you to read my previous post on the topic to get a better understanding of how the framework works.

Macaw Framework is designed to be simple and easy to use, with sensible defaults that make it easy to get started. It's also highly customizable, with a wide range of options for configuring your application to meet your needs. Whether you're building a simple API or a complex web application, Macaw Framework can help you get the job done quickly and efficiently.

## Step 0: Create and Configure Your `application.json` File

Before we dive into building our CRUD application, we need to create and configure our `application.json` file. This file will contain configuration options for our Macaw Framework application.

Create a new file called `application.json` in the root directory of your project and add the following contents:

```json
{
  "macaw": {
    "threads": 10,
    "bind": "0.0.0.0",
    "port": 8080,
    "cache": {
      "cache_invalidation": 10,
      "ignore_headers": [
        "Postman-Token"
      ]
    },
    "prometheus": {
      "endpoint": "/metrics"
    },
    "rate_limiting": {
      "window": 10,
      "max_requests": 10
    }
  }
}
```

This file contains various options that will be used by Macaw Framework to configure your application. For example, the `threads` option specifies the number of threads that should be used by the application, while the `cache` option configures caching behavior. You can adjust these options to suit the needs of your application.

Once you've created your `application.json` file, you're ready to start building your CRUD application using Macaw Framework!

## Step 1: Set Up Your Environment

To get started, create a new directory for your project and navigate into it. Then, add a Gemfile with the following content:

```ruby
# frozen_string_literal: true

source "https://rubygems.org"

gem "sqlite3"

gem "yaml"

gem "json"

gem "macaw_framework"

gem "activerecord"

```

For this demo project I'm using a SQLite3 database. You can find the database and everything else in the project's repository.

Next, run bundler in order to install the dependencies:

```shell
bundle install
```

Using Bundler makes it easier to manage dependencies for your project, and ensures that all team members are using the same versions of the required gems. Once you have your dependencies installed, you're ready to start building your application with Macaw Framework!

## Step 2: Define Your Person Model

Create a new file called `person.rb` in a subdirectory called lib/entity. This file will define your Person model, which represents a person in your database.

```ruby
class Person < ActiveRecord::Base
  table_name = 'people'
end
```

## Step 3: Create Your App File

Create a new file called `app.rb` in the root directory of your project. This file will be the entry point for your application, and it will define your Macaw Framework endpoints.

```ruby
# frozen_string_literal: true

require 'macaw_framework'
require 'active_record'
require 'yaml'
require_relative './lib/entity/person'
require_relative './lib/errors/unprocessable_body_error'

# Configuring ActiveRecord
db_config = File.open('./db/database.yaml')
ActiveRecord::Base.establish_connection(YAML.safe_load(db_config, aliases: true))

# Instantiating MacawFramework Class
server = MacawFramework::Macaw.new
```
## Step 4: Define Your GET Endpoint

The first endpoint we'll define will allow us to retrieve a list of all people in the database. Add the following code to your `app.rb` file:

```ruby
# Defining a GET endpoint to list all persons in the database
server.get('/people', cache: true) do |_context|
  return JSON.pretty_generate(Person.all.as_json), 200, {"Content-Type" => "application/json", "random-header" => "random value"}
end
```
## Step 5: Define Your GET by Id Endpoint

The second endpoint we'll define will allow us to retrieve a specific person from the database by their ID. Add the following code to your `app.rb` file:

```ruby
# Defining a GET endpoint to recover person with provided id
server.get('/people/:person_id') do |context|
  return JSON.pretty_generate(Person.where(id: context[:params][:person_id]).first.as_json), 200
end
```

## Step 6: Define Your POST Endpoint

The third endpoint we'll define will allow us to create a new person in the database. Add the following code to your `app.rb` file:

```ruby
server.post('/add_new_person') do |context|
  begin
    parsed_body = JSON.parse(context[:body])
    name = parsed_body['name']
    age = parsed_body['age']
    raise UnprocessableBodyError if name.nil? || age.nil?

    Person.create!(name: name, age: age)
    return JSON.pretty_generate({ message: "Person created with success!!!" }), 201
  rescue UnprocessableBodyError => e
    return JSON.pretty_generate({ error: e }), 422
  rescue StandardError => e
    return JSON.pretty_generate({ error: e }), 500
  end
end
```

This endpoint will create a new `Person` record in the database based on the provided request body, which should be a JSON object with `name` and `age` properties. If the request body is missing either property, we'll raise our custom `UnprocessableBodyError` and return a `422 Unprocessable Entity` status code with an error message.

Note that we're also raising a new custom error class, `UnprocessableBodyError`, if the request body is missing the required properties. To define this error class, create a new file called `unprocessable_body_error.rb` in your `lib/errors` directory with the following contents:

```ruby
class UnprocessableBodyError < StandardError
  def initialize(
    msg = 'Please inform a body on your request in JSON format with a "name" and "age" properties'
  )
    super
  end
end
```

This error will be rescued in our `PATCH` and `POST` endpoint and return a `400 Bad Request` status code with an error message if the request body is missing the required properties.

## Step 7: Define Your PATCH Endpoint

The fourth endpoint we'll define will allow us to update an existing person in the database. Add the following code to your `app.rb` file:

```ruby
server.patch('/people/:person_id') do |context|
  raise UnprocessableBodyError.new unless context[:params][:person_id]

  body = JSON.parse(context[:body])
  name = ActiveRecord::Base.connection.quote(body["name"])
  age = ActiveRecord::Base.connection.quote(body["age"])
  Person.update(
    context[:params][:person_id].to_i,
    name: name,
    age: age
  )
  [JSON.pretty_generate({ message: "Updated person #{context[:params][:person_id]}" }), 200]
rescue UnprocessableBodyError
  ["Person with id #{context[:params][:person_id]} does not exist", 400]
rescue StandardError => e
  [JSON.pretty_generate(e.message), 500]
end
```

## Step 8: Define Your DELETE Endpoint

The fifth and final endpoint we'll define will allow us to delete an existing person from the database. Add the following code to your `app.rb` file:

```ruby
# Defining a DELETE endpoint to delete an existing person in the database
server.delete('/delete_person') do |context|
  begin
    parsed_body = JSON.parse(context[:body])
    id = parsed_body['id']
    raise UnprocessableBodyError.new('Please inform a JSON body with an "id" parameter') if id.nil?

    person = Person.find_by(id: id.to_i)
    if person.nil?
      return JSON.pretty_generate({ error: "Person with ID #{id} not found." }), 404
    else
      person.destroy
      return JSON.pretty_generate("Person with ID #{id} deleted."), 200
    end
  rescue UnprocessableBodyError => e
    return JSON.pretty_generate({ error: e.message }), 422
  rescue StandardError => e
    return JSON.pretty_generate({ error: e }), 500
  end
end
```
## Step 9: Start Your Server

Finally, add the following code to your `app.rb` file to start the server:

```ruby
# Start Server
server.start!
```

Now that you've defined all of your endpoints, you're ready to start your server and test your application! You can do this by running the following command:

```shell
ruby app.rb
```
Your server will start running on http://localhost:8080/. You can use tools like Postman or cURL to send requests to your endpoints and test your application.

Now that we've defined all of our endpoints, we're ready to start our server and test our application!