REST Turns Humans Into Database Clients

🌶🔥🌶🔥🌶🔥🌶🔥

REST helps us solve a problem

We want to put user interactions into a relational database

REST has a quick answer for many user needs

As long as those needs sound like CRUD

💁‍♂️BOB: I want to give Peter access to the system

🤖REST: POST /users params: {user: {first_name: "Peter", last_name: "Parker"}}

💁‍♂️BOB: Peter got married! Let's update his last name

🤖REST: PATCH /users/1 params: {user: {last_name: "Watson"}}

Familiar-looking REST controller


class UsersController < ApplicationController
  def update
    user = User.find(params[:id])

    user.update(user_params)
  end

  private

  def user_params
  	params.require(:user).permit(:first_name, :last_name)
  end
end
          

What if our user's desires aren't a single operation on a single database table?

💁‍♂️BOB:Peter's a straight-shooter with upper management written all over him. I'd like to promote Peter to manager. Managers should automatically get certain permissions

🤖REST:PATCH /users/1 params: {user: {role: "manager"}}


class UsersController < ApplicationController
  def update
    user = User.find(params[:id])
    if params["user"]["role"] && current_user.admin?
      user.update(role: params["users"]["role"])
      user.permissions.create(
    		{name: "Access To Nice Coffee Machine"}
      )
    end

    user.update(user_params)
  end

  private

  def user_params
  	params.require(:user).permit(:first_name, :last_name)
  end
end
          

👩‍⚖️Objection!

We use new REST resources to solve problems like this!


class UserPromotionsController < ApplicationController
  # POST /users/#{user_id}/promote
  def create
    user = User.find(params[:user_id])
    if current_user.admin?
      user.update(role: "admin")
      user.permissions.create(
    		{name: "Access To Nice Coffee Machine"}
      )
    end
  end
end
          

I call that a REST Contortion 🔀

Resources that are not be backed by a database table. It's a way to accommodate changes that aren't 1:1 with database tables

I think we can do a little better

REST contortions still attempt to make user actions look like database operations.

Users are not database clients!

Users want to promote other users, not insert user promotions

So what do we do now?

POST /users/promote_#{bob}_to_admin

Just kidding!

Here's one alternative

POST /api/graphql

Let's let our API be defined by user actions, not database operations


module Types
  class MutationType < Types::BaseObject
    field :add_user, mutation: Mutations::AddUserMutation
    field :update_user, mutation: Mutations::UpdateUserMutation
    field :promote_user, mutation: Mutations::PromoteUserMutation
  end
end
          

This is what graphql-ruby mutations look like


module Mutations
  class ChangeRoleMutation < BaseMutation
    argument :user_id, ID

    field :user, Types::UserType
    field :errors, [ErrorData], null: false

    def resolve(user_id)
      user_promotion = UserPromotion.new(
        current_user: context[:current_user],
        user_id: user_id
      )
      user_promotion.perform!

      {
        user: user_promotion.user,
        errors: user_promotion.errors
      }
    end
  end
end
          

A very testable object


class UserPromotion
  extend ActiveModel::Naming

  attr_accessor :current_user, :user
  attr_reader :errors

  def initialize(current_user:, user_id:)
    @errors = ActiveModel::Errors.new(self)
    @current_user = current_user
    @user = User.find(user_id)
  end

  def perform!
    if permitted?
      user.update!(role: "admin")
      user.permissions.create(
        {name: "Access To Nice Coffee Machine"}
      )
    else
      errors.add(:current_user, "cannot promote other users")
    end
  end

  private

  def permitted?
    current_user.admin?
  end
end
          

Sumary

REST is great, but starts to break when user interactions aren't 1:1 with database operations

graphql has good answers if you are building an API + client(s) application

Thank You!