Rails Validations
Tags: rails, ruby
Published: 2020-06-14 00:39:06

When creating a backend for any project it is important to think about how incoming data will be collected and organized. If there are pieces of data that are essential for your app to function properly it is useful to use validations.

I will discuss the process of setting up validations in Rails and how to structure your app controllers to respond with appropriate data for a non-Rails frontend.

When constructing your Rails models you have the ability to add many different types of validations - including custom validations.

First I'm going to demonstrate how absent validations could negatively affect your code. Here's a schema for a Patient class: 

```ruby
create_table "patients", force: :cascade do |t|
  t.string "name"
  t.date "dob"
  t.string "email"
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
end
```

If I wanted to add a new Patient to my database I could enter the rails console and perform 'Patient.create(...)' OR 'Patient.new(...)' combined with 'Patient.save'. Without validations, if I omit one or more of the three attributes of Patient, it will still save to my database. For example:

```ruby
Patient.create(:name => "Max")

-> Patient Create (7.7ms)  INSERT INTO "patients" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "Max"], ["created_at", "2020-06-13 22:37:39.727082"], ["updated_at", "2020-06-13 22:37:39.727082"]]
   (12.5ms)  COMMIT
 => #<Patient id: 6, name: "Max", dob: nil, email: nil, created_at: "2020-06-13 22:37:39", updated_at: "2020-06-13 22:37:39"> 
```

As you can see, a new Patient without a dob and an email still persisted to my database, with a Patient ID of 6. This could create problems down the line as it is important to have normalized data, which in turn creates a better user experience and leads to less bugs. 

Preventing this issue from occurring is relatively simple. It requires that each model includes a 'validates' line for each attribute that needs to be checked, and that your controllers anticipate how to respond to improper POSTs and PATCHes.

The first step to fix the Patient example is to modify the model with validations:

```ruby
class Patient < ApplicationRecord
  validates :name, presence: true
  validates :dob, presence: true
  validates :email, presence: true
  validates :email, uniqueness: true
end
```

Sidenote: There are many different types of built-in validations that can be used and you may also create custom validations. Refer to [Active Record Validations](https://guides.rubyonrails.org/active_record_validations.html) for more info.

In my last code snippet I created validations to ensure that each of the attributes are present for any instance of Patient and I added an additional validation to check that the patient's email is unique. 

Now when I try to perform the same console action as before I encounter an error:

```ruby 
Patient.create(:name => "Max")

Patient Exists? (0.4ms)  SELECT 1 AS one FROM "patients" WHERE "patients"."email" IS NULL LIMIT $1  [["LIMIT", 1]]
   (11.2ms)  ROLLBACK
 => #<Patient id: nil, name: "Max", dob: nil, email: nil, created_at: nil, updated_at: nil> 
```

This time the patient instance did not persist to the database (Patient ID is returned as 'nil'). Our validations clearly worked! 

When hitting a Rails API with a different frontend it is important to think about how data will be returned to a user. As you can see in our previous code snippet, an ActiveRecord instance was returned for our 'create' command, even though the instance was not persisted to the database. We would not want to send back information from this ActiveRecord instance because that could incorrectly indicate to a user that their data was saved. To prevent this we would write 'create' and 'update' controls that check if data was saved before sending it back to the user:

```ruby
def create
  patient = Patient.new(patient_params)
  if patient.save
    render json: patient
  else
    render json: patient.errors.messages
  end
end 

def update
  patient = Patient.find(params[:id])
  patient.update(patient_params)
  if patient.valid?
    render json: patient
  else
    render json: patient.errors.messages
  end
end
```

Now when a user tries to create or update a record with invalid data, our backend will send an error (error messages can also be customized - [Validation Errors](https://guides.rubyonrails.org/active_record_validations.html#working-with-validation-errors)).

This was a simple run-through on how to set up validations and modify controllers accordingly, but feel free to peruse the documentation for more advanced validation options!

Sources:

1. [Active Record Validations](https://guides.rubyonrails.org/active_record_validations.html)