Benito Serna Tips and tools for Ruby on Rails developers

A decoupled CRUD on Rails

March 18, 2019

As I said in the last post as ruby/rails developers is very common to start our projects with something very similar to the Rails scaffold…

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  end

  def index
    @posts = Post.all
  end

  #....
end

… In that post I show you an example of a simple blog app, with a functionality very similar to what a Rails scaffold can do but without coupling our code to Active Record.

The example was using Sinatra, but not to tell you not tu use Rails…

But to tell you that you can write your code in a way, where you can delay decisions like “Which framework should I use?”, or “Which ORM library should I use?”…

You don’t have to make does decisions since the beginning… Or more than that… You don’t have to keep your first day decisions forever!…

If you design your software in a different way…

The same app but with Rails.

You can see the app working here and the code here.

About the design

The behavior of the app is exposed by stateless module functions in the module Blog that lives on lib/blog.rb. And is actually the same code used in the last example.

module Blog
  def self.list_posts(store)
    store.all.sort_by(&:created_at).reverse
  end

  def self.get_post(id, store)
    store.find(id)
  end

  def self.new_post_form
    PostForm.new
  end

  def self.create_post(params, store, current_time = Time.now)
    ProcessPostForm.(params) do |form|
      store.create(form.to_h.merge(created_at: current_time))
    end
  end

  def self.edit_post_form(id, store)
    PostForm.new(store.find(id))
  end

  def self.update_post(post_id, params, store)
    ProcessPostForm.(params) do |form|
      store.update(post_id, form.to_h)
    end
  end

  def self.delete_post(post_id, store)
    store.destroy(post_id)
  end

  #...
end

There is a test file for each CRUD action…

And there is a test file for our “store”…

And now the behavior of our app is exposed on the PostsController of our Rails app

class PostsController < ApplicationController
  def index
    posts = Blog.list_posts(store)
    render :index, locals: { posts: posts }
  end

  def new
    form = Blog.new_post_form
    render locals: { form: form }
  end

  def create
    status = Blog.create_post(params, store)

    if status.success?
      redirect_to posts_path
    else
      render :new, locals: { form: status.form }
    end
  end

  def show
    post = Blog.get_post(params[:id], store)
    render locals: { post: post }
  end

  def edit
    form = Blog.edit_post_form(params[:id], store)
    render locals: { form: form, post_id: params[:id] }
  end

  def update
    status = Blog.update_post(params[:id], params, store)

    if status.success?
      redirect_to posts_path
    else
      render :edit, locals: { form: status.form, post_id: params[:id] }
    end
  end

  def destroy
    Blog.delete_post(params[:id], store)
    redirect_to posts_path
  end

  private

  def store
    BlogStore
  end
end

Don’t you think that is nice to be able to change our framework decision? Don’t you think that is nice to be able to express our business logic decoupled from a specific database technology?

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.