# Repository Pattern In Rails

> A look at how we can implement a Railsy-way of the Repository Pattern in our Rails apps.

- Canonical: https://dennisokeeffe.com/blog/2022-02-22-repository-pattern-in-rails
- Published: 2022-02-22
- Updated: 2022-02-22
- Tags: ruby, ruby-on-rails

This post will explore an approach to a repository pattern to be implemented in Ruby on Rails 7.

It is by no mean a perfect implementation - in fact, it is not a "by-the-books" implementation. Ultimately it is just an abstraction so that we have a repository to handle the persistence which in turn also improves our separation of concerns and can help us to make our code more testable.

The blog post will be the first of a two-part blog post series. The follow-up post will focus on implementing the [`dry-validation`](https://dry-rb.org/gems/dry-validation/1.6/) gem, hence the name for the repo in this post. It is not necessary to name your Rails project the same as I am doing today.

Source code can be found [here](https://github.com/okeeffed/demo-repo-pattern-dry-validation).

## Prerequisites

1. Basic familiarity with setting up a [new Rails project](https://guides.rubyonrails.org/getting_started.html).
2. Basic familiarity with [RSpec](https://rspec.info/) which we will be using for making class doubles of our repository class.

## Getting started

We will use Rails to initialize the project `demo-repo-pattern-dry-validation`:

```s
# Create a new rails project
$ rails new demo-repo-pattern-dry-validation
$ cd demo-repo-pattern-dry-validation

# Make our example Post model
$ ./bin/rails g model Post title rating
# Create our controller for the example with a create, index and show method
$ ./bin/rails g controller posts create index show

# Create a folder for our repos
$ mkdir -p app/repositories
# Add our Post repo
$ touch app/repositories/posts_repository.rb

# Prepare for our testing
$ bundler add rspec-rails --group="development,test"
# Install required files
$ ./bin/rails g rspec:install
# Create file for our testing
$ mkdir -p spec/controllers
$ touch spec/controllers/posts_controller_spec.rb

# Ensure that we create our DB and run our migrations
$ ./bin/rails db:create db:migrate

# Start the server
$ ./bin/rails s
```

At this stage, our server is configured to run. Given that we are focusing more on the API for this tutorial, we will need to also make an adjustment to our configuration.

## Configuring our application

For the demo, we will want to set `config.action_controller.default_protect_from_forgery` to `false`.

In the `config/application.rb` file, update it to the following:

```rb
require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module DemoRepoPatternDryValidation
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.0

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")
    config.action_controller.default_protect_from_forgery = false
  end
end
```

> Note: This is purely for demonstration purposes, and should not be set to `false` in a production environment when CSRF tokens are required.

At this stage, we are ready to start creating our repository and configuring the methods on our posts controller.

## Creating our repository class

The idea behind the repository class is to abstract our persistence layer from our controller. This means that any of our calls that need to read or write to the database will be abstracted to our repository class.

In the `app/repositories/posts_repository.rb`, we will add some methods to help us create a post, get all posts and to get a particular post by its ID:

```rb
class PostsRepository
  class << self
    def create(title:, rating:)
      post = Post.new(title: title, rating: rating)
      post.save!

      # `save!`` will raise if there is an error, so we assume here
      # that we were successful and return the post to follow RESTful conventions.
      # @see https://restfulapi.net/http-status-200-ok/
      post
    end

    def find_all
      Post.all
    end

    def find_by_id(id:)
      Post.find_by(id: id)
    end
  end
end
```

The above has three functions:

1. `create` - to create a new post.
2. `find_all` - to get all posts.
3. `find_by_id` - to get a particular post by its ID.

It is worth noting that concepts such as validation are omitted from this blog post and will be covered in the next blog post, but for now we are just making the abstraction.

I am purposely using named parameters to enforce more specific invocations of the method, but it is up to you whether you want to make the arguments more generic and modular.

At this point, we are ready to update our controller.

## The posts controller

Inside of `app/controllers/posts_controller.rb`, update the code to the following:

```rb
require 'posts_repository'

class PostsController < ApplicationController
  def create
    post = PostsRepository.create(title: params[:title], rating: params[:rating])

    render json: post, status: :ok
  rescue ActiveRecord::ActiveRecordError
    render json: { message: 'Internal server error' }, status: :internal_server_error
  rescue StandardError
    render json: { message: 'Internal server error' }, status: :internal_server_error
  end

  def index
    posts = PostsRepository.find_all
    render json: posts, status: :ok
  rescue ActiveRecord::ActiveRecordError
    render json: { message: 'Internal server error' }, status: :internal_server_error
  rescue StandardError
    render json: { message: 'Internal server error' }, status: :internal_server_error
  end

  def show
    post = PostsRepository.find_by_id(id: params[:id])
    render json: post, status: :ok
  rescue ActiveRecord::RecordNotFound
    render json: { message: 'Not found' }, status: :not_found
  rescue ActiveRecord::ActiveRecordError
    render json: { message: 'Internal server error' }, status: :internal_server_error
  rescue StandardError
    render json: { message: 'Internal server error' }, status: :internal_server_error
  end
end
```

In the code above, we are implementing the three methods `create`, `index` and `show` and using our repository class to make the call to the database.

All errors are being handled by the `rescue` blocks. In the next post, I will be updating the repository to handle the errors there and returning `Success` and `Failure` monads, but for now I am bubbling up the errors directly to our controller and handling them there.

At this point, all we need to do is update our routes configuration.

## Updating our routes configuration

Inside of `config/routes.rb`, update the code to the following:

```rb
Rails.application.routes.draw do
  resources :posts,  only: [:index, :show, :create]
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  # root "articles#index"
end
```

Here we are essentially creating a `posts` resource that will only allow our `index`, `show` and `create` methods that we have implemented.

Since we set our CSRF requirement to `false` in our application configuration, I can go ahead and use [Postman](https://www.postman.com/) to test out our API.

First, I can create a new post by sending a POST request to `/posts`:

![Creating a post](https://cdn.dennisokeeffe.com/assets/2022-02-22-repository-pattern-in-rails/1-create-post.webp)

Then we can test out fetching all posts:

![Fetching all posts](https://cdn.dennisokeeffe.com/assets/2022-02-22-repository-pattern-in-rails/2-get-all-posts.webp)

Finally, we can test out fetching a particular post by its ID (in our case, by ID 2):

![Fetching a specific post by ID](https://cdn.dennisokeeffe.com/assets/2022-02-22-repository-pattern-in-rails/3-get-post-by-id.webp)

Perfect! It looks like our Rails API system is working with our `PostsRespository` class!

## Testing our controller

The beauty of separating our persistence layer from our controller is that we can test our controller without having to worry about our persistence layer.

In this particular post, I will demonstrate this by using RSpec class doubles to ensure that we are testing all possible cases of our controller for the `#index` and `#show` routes.

> Note: As previously mentioned, there are posts following on from this that will make adjustments to the tests and how our controller is tested (as well as the persistence layer). Read on after taking a grain of critical salt.

In our `spec/controllers/posts_controller_spec.rb`, we will add the following:

```rb
require 'rails_helper'
require 'posts_repository'

RSpec.describe PostsController, type: :controller do
  describe 'GET #index' do
    context 'successful responses' do
      it 'returns empty successful array when there are no posts' do
        stubbed_response = []
        posts_respository_klass = class_double(PostsRepository, find_all: stubbed_response).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_all)

        get :index
        expect(response).to have_http_status(:success)
        expect(response.parsed_body).to eq(stubbed_response)
      end

      it 'returns correct array when there are posts' do
        stubbed_response = [{
          id: 1,
          title: 'Cool post',
          rating: 'Good'
        }]
        posts_respository_klass = class_double(PostsRepository, find_all: stubbed_response).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_all)

        get :index
        expect(response).to have_http_status(:success)
        expect(response.parsed_body).to eq(JSON.parse(stubbed_response.to_json))
      end
    end

    context 'unsuccessful responses' do
      it 'returns 500 when there is an ActiveRecord::ActiveRecordError' do
        res = { message: 'Internal server error' }
        posts_respository_klass = class_double(PostsRepository).as_stubbed_const
        allow(posts_respository_klass).to receive(:find_all).and_raise(ActiveRecord::ActiveRecordError)

        expect(posts_respository_klass).to receive(:find_all)

        get :index
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end

      it 'returns 500 when there is a StandardError' do
        res = { message: 'Internal server error' }
        posts_respository_klass = class_double(PostsRepository).as_stubbed_const
        allow(posts_respository_klass).to receive(:find_all).and_raise(StandardError)

        expect(posts_respository_klass).to receive(:find_all)

        get :index
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end
    end
  end

  describe 'GET #show' do
    # ... TODO
  end

  describe 'POST #create' do
    # ... TODO
  end
end
```

In the above, I am separating the tests to describe each route into its own `describe` block.

After, I am separating the context based on expected successful and unsuccessful responses.

I am using the RSpec `class_double` method to create a class double that will be used to mock the `PostsRepository` class. This means that we do not requiring persisting anything to the database, and that we can also now test all of our controller responses to ensure that they are working as expected.

The RSpec mocking also allows us the granularity of setting which error to raise, which you will notice in our `#show` routes enables us to check for the correct error response based on the error raised.

## Testing our `#show` route

Finally, we can follow a similar pattern to test all the possible scenarios for our `#show` route.

> Note: Here that I am also handling the 404 scenario which makes it more interesting than the above 500-only error responses.

```rb
require 'rails_helper'
require 'posts_repository'

RSpec.describe PostsController, type: :controller do
  describe 'GET #index' do
    # ... omitted for brevity
  end

  describe 'GET #show' do
    context 'successful responses' do
      it 'returns correct array when there is a post with the expected ID' do
        stubbed_response = {
          id: 1,
          title: 'Cool post',
          rating: 'Good'
        }
        posts_respository_klass = class_double(PostsRepository, find_by_id: stubbed_response).as_stubbed_const

        expect(posts_respository_klass).to receive(:find_by_id).with(id: '1')

        get :show, params: { id: '1' }
        expect(response).to have_http_status(:success)
        expect(response.parsed_body).to eq(JSON.parse(stubbed_response.to_json))
      end
    end

    context 'unsuccessful responses' do
      it 'returns 500 when there is an ActiveRecord::ActiveRecordError' do
        res = { message: 'Internal server error' }
        posts_respository_klass = class_double(PostsRepository).as_stubbed_const
        allow(posts_respository_klass).to receive(:find_by_id).and_raise(ActiveRecord::ActiveRecordError)

        expect(posts_respository_klass).to receive(:find_by_id).with(id: '1')

        get :show, params: { id: '1' }
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end

      it 'returns 500 when there is a StandardError' do
        res = { message: 'Internal server error' }
        posts_respository_klass = class_double(PostsRepository).as_stubbed_const
        allow(posts_respository_klass).to receive(:find_by_id).and_raise(StandardError)

        expect(posts_respository_klass).to receive(:find_by_id).with(id: '1')

        get :show, params: { id: '1' }
        expect(response).to have_http_status(:internal_server_error)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end

      it 'returns 404 when there is no matching post' do
        res = { message: 'Not found' }
        posts_respository_klass = class_double(PostsRepository).as_stubbed_const
        allow(posts_respository_klass).to receive(:find_by_id).and_raise(ActiveRecord::RecordNotFound)

        expect(posts_respository_klass).to receive(:find_by_id).with(id: '1')

        get :show, params: { id: '1' }
        expect(response).to have_http_status(:not_found)
        expect(response.parsed_body).to eq(JSON.parse(res.to_json))
      end
    end
  end

  describe 'POST #create' do
    # ... omitted
  end
end
```

Perfect! So now we not only have some specs but those specs also test each identified response from the server and enable us to address them individually.

I will leave it to another post (or you) to implement the `#create` tests as there will be some refactoring to do once I bring in the monads and our validation.

## Summary

Today's post demonstrated how to separate our persistence layer from our business layer in the controller. Separating the persistence layer comes with benefits for testing and a separation of concerns, but it does also mean more bloat in our code. That being said, it means that we can keep a lot of our more custom queries to stay out of the model and be placed into its own class.

In the next post, I will be introducing `dry-validation` to start adding in more validation as well as cleaning up with `dry-monads` so that we are handling errors better at the source and more gracefully passing them up to the controller.

## Resources and further reading

- [Rails getting started](https://guides.rubyonrails.org/getting_started.html)
- [Rails model patterns and anti-patterns](https://blog.appsignal.com/2020/11/18/rails-model-patterns-and-anti-patterns.html)
- [Source code](https://github.com/okeeffed/demo-repo-pattern-dry-validation)
- [RSpec - Testing Rails Controllers](https://relishapp.com/rspec/rspec-rails/docs/controller-specs)
- [Postman](https://www.postman.com/)

_Photo credit: [`joelfilip`](https://unsplash.com/@joelfilip)_
