What is a CRUD API?

Anika Bernstein
|
March 28, 2022

Before I entered the world of computer programming, the terms API or CRUD didn’t mean very much to me. Little did I know that CRUD APIs were a huge part of my digital life working under the hood.

How does your banking app use your phone’s face recognition feature to log you into your account? How does Twitter allow only your followers to see your posts? An API (application programming interface) is responsible for handling these permissions and exchanges at a high level. And what about a CRUD API? Well, APIs act as both the referee and gatekeeper of information, and CRUD functions allow specific interactions between the client and the database.

I’m going to explain what a CRUD API is and how to apply it in different use cases to protect and interact with data in very specific ways. This post will introduce and explain CRUD and authentication to programmers and curious dilettantes.

What Is a CRUD API?

CRUD stands for create, read, update, and delete. These functions are the four pillars of a complete CRUD API (and full-stack application, for that matter).

Let's look at a sample healthcare company, Shmealth, which needs to store, access, and update its patients’ health records accurately and securely. Follow along to see how each of the CRUD functions operate in Shmealth's API built in Ruby on Rails.

CRUD: Create

The C in CRUD stands for create, which has many nuances. We need to add a new patient to Shmealth's database before a physician is able to view their vitals or update their prescriptions. Let’s discuss this action first.

Your CRUD API should first be architected to relate various resources to one another. Then, you can start creating those resources either as seeds directly into the back end or through an HTTP POST request from a front end.

For those of you who need a quick refresher on what it means to seed your database, when you create a new record, you're generating an instance of that resource's class. Capitalization and pluralization matter when talking about instances and classes.

Use Cases

In this scenario, Sadie Baker will be a new Shmealth patient and, therefore, an instance of the Patient class. Before we can view or interact with any data, it must first exist in the database. Other use cases include:

  • Adding a new doctor to Shmealth's system,
  • Creating an appointment, and
  • Adding a new medication.

In relational databases, some resources may need to be created before others (e.g., an instance of the Appointments class cannot be created for Sadie until she's in the system).

When new seeds are created, some APIs automatically assign an identification number to those instances (e.g., a health record number or an appointment confirmation number). It's important that this ID number remains unique. If you’re just starting out, don't touch or change these numbers. We'll go over how they can be used in other CRUD API actions later.

Conventions

You would use different conventions for the create function depending on whether you're working in the front end or back end. For the front end, the REST and HTTP method is "POST". For the back end, the SQL operation is "INSERT".

Accuracy

Inaccurate or falsified information can have colossal consequences. In healthcare, it's extremely important that patients' health data (such as prescriptions and blood test results) are protected.

As such, you need to safeguard against user errors. When generating a new record in a database (e.g., adding a new patient), each property of the record (the patient's information) has to be entered as the same data type defined in the migration file (i.e., string, date, boolean, float, etc.).

class CreatePatients < ActiveRecord::Migration[6.1]

 def change

   create_table :patients do |t|

     t.string :firstname

     t.string :lastname

     t.date :birthday

     t.boolean :is_active

     t.float :monthly_premium

     t.string :username

     t.string :password_digest

     t.timestamps

   end

 end

end

Don't forget to use validations in your Models, as well as strong_params and status codes in your Controllers.

Here, I use the #create method:

class PatientsController < ApplicationController

 def create

   @new_patient = Patient.new patient_params

   if @new_patient # if new instance of @new_patient from line 4 returns truthy with no errors

     @new_patient.save # then save

     render json: @new_patient, status: :created

   else

     render json:

       { error: "Oops! Something went wrong. Please check patient's information and try again" },

       status: :unprocessable_entity

   end

 end

 private

 def patient_params

   params.require(:patient).permit(:firstname, :lastname, :birthday, :is_active, :monthly_premium, :username,:password)

 end

end

Notice that I use Patient.new to check the validity of a @new_patient instance before saving the record.

Security

As with many databases, Shmealth's health records need to be accurate and protected. This is why developers have created and utilized secure accounts that require encrypted credentials (i.e., signup and login). Sadie shouldn't be able to log in to her patient portal and create a new prescription for herself.

Through the use of authorization and authentication in the API, only authorized users have access to creating certain records. Every time an HTTP request is made from the patient's portal, an authentication method in the API filters patient-specific actions and information from the database.

When logging in to an account, the wording of error messages is important, too. When Sadie enters her username and password and clicks "Log In," her account is making a POST request. If the password she enters doesn't match the hashed password used to create her account, it's less secure to be more specific in the error message. In this case, we'd want to use something vague like "Username and password do not match" rather than "Incorrect password" or "Incorrect username."

CRUD: Read  

The R in CRUD stands for read, which simply retrieves data from the database and displays it. From a programmer's perspective, this is the simplest and safest CRUD action. As database architects, we can decide which routes allow the view of all, some, or only single records.

Use Cases

Some use cases for the read function include:

  • Viewing upcoming appointments,
  • Reviewing a patient's cholesterol levels over time, and
  • Reading a doctor's virtual action plan curated for the patient.

Conventions

Again, you'll use different methods depending on which side of the application you're working on. For the front end, the REST and HTTP method is "GET". For the back end, the SQL operation is "SELECT".

Let's Take a Look at the API Configuration

Let's say Dr. Rebecca Hernandez wants to view of all Sadie's current prescriptions (starting with the most recently prescribed in descending order). So, she logs in to her physician portal and clicks on "View All Prescriptions." What's happening behind the scenes to allow this list to populate on her screen?

The Patient Model has a custom instance method that finds all prescriptions relating to Sadie's ID number and filters out the inactive medications, which I've called sorted_meds.

class Patient < ApplicationRecord

 has_secure_password

 has_many :appointments

 has_many :prescriptions

 has_one :doctor, through: :appointments

 has_many :vitals, through: :appointments

 validates :username, presence: true, uniqueness: true

 def sorted_meds

   current = self.prescriptions.where(active: true)

   sorted = current.sort_by { |med| [med.start_date]}

   sorted.reverse

 end

end

The Routes file would also need to specify the action associated with an endpoint for this method. In this case, I've used /current_meds. When creating endpoints, we want to design with intent; it's important that they are specific enough to a particular resource and legible.

Rails.application.routes.draw do

 resources :prescriptions

 resources :members

 resources :doctors

 get '/current_meds', to: 'patients#current_meds'

end

Notice how I used the HTTP method GET in the above code.

Lastly, the Patients Controller has a custom method I've created, called current_meds. I use the same nomenclature as the endpoint for readability.

class PatientsController < ApplicationController

 def current_meds

   @patient = Patient.find params[:id]

   render json: {patient: @patient}, methods: [:sorted_meds], status: :ok

 end

end

When Dr. Hernandez clicks that "View Current Prescriptions" button, this sends an HTTP request to Shmealth's back end (to the /current_meds endpoint we created). The API's authenticator grants Dr. Hernandez access to all of the sorted_meds related to Sadie's ID number, pulls the data from the SQL database, and returns that information in JSON to Shmealth's front end, which parses and reformats to some customized view on Dr. Hernandez's screen—all within milliseconds.

To view all medications prescribed to all patients at Shmealth, I use the standard #index method in the Prescriptions Controller:

class PrescriptionsController < ApplicationController

 def index

   @medication = Prescription.all

   render json: @medication, status: :ok

 end

end

To view information about a single medication, the standard method is #show, which requires the ID number of that specific medication.

Accuracy and Security

In terms of viewing data from an API, security measures pertain mostly to the credentials used to retrieve this data. Sadie shouldn't be able to see which patients Dr. Hernandez has, just as nonmembers shouldn't be able to access exclusive Shmealth resources.

Here's an example of an authentication method in the Application Controller:

class ApplicationController < ActionController::API

 before_action :authenticate

 def authenticate

   auth_header = request.headers[:Authorization]

   if !auth_header

     render json: {error: 'Auth bearer token header is required'}, status: :forbidden

   else

     token = auth_header.split(' ')[1]

     secret = 'XXXXXXX'

     begin

       decoded_token = JWT.decode token, secret

       payload = decoded_token.first

       @user = User.find payload['user_id'] #instance variable that can be carried across methods

     rescue

       render json: {error: 'Unrecognized auth bearer token'}, status: :forbidden

     end

   end

 end

end

def login

   @user = Patient.find_by username: params[:patient][:username]

   if !@user

     render json: {error: 'Invalid username or password'}, status: :unauthorized

   else

     if !@user.authenticate params[:patient][:password]

       render json: {error: 'Invalid username or password'}, status: :unauthorized

     else

       payload = {patient_id: @user.id}

       secret = 'XXXXXXX'

       @token = JWT.encode payload, secret

       render json: {token: @token, patient: @user}, status: :ok

     end

   end

 end

Other security measures should be taken on the front end, such as timed logout after two minutes of inactivity, etc.

CRUD: Update

The U stands for update. This means changing existing data to a new value of the same data type and overwriting the old value in the database.

Depending on how databases and APIs are set up, a user may not be able to see previously edited versions of data easily. Updating data, especially in healthcare, must be set up with a robust authentication so patients can't update the dosage of their medications or values of their blood test, etc. Similarly, physicians shouldn't be able to update a patient's username or password. This could cause legal issues on top of a multitude of other consequences.

Use Cases

Some use cases for the update function include:

  • Changing the patient's mailing address,
  • Switching primary doctors,
  • Rescheduling an appointment time, and
  • Increasing the patient's monthly premium.

Remember when I said identification numbers would be useful? These come into play any time we are targeting a specific instance or record. Below, I use the #update method to find an existing appointment by ID (or confirmation number). Then, I take in the new date and time parameters for that appointment and update the key-value properties accordingly in the database. Without being able to locate this appointment by ID, the database would have no idea which appointment we wanted to update.

class AppointmentsController < ApplicationController

def update

   @appointment = Appointment.find params[:id]

   @appointment.udpate date: params[:date], time: params[:time]

   render json: @appointment, status: :updated

 end

end

Conventions

The REST and HTTP method when working in the front end is "PATCH", "PUT". The back-end SQL operation is "UPDATE".

Security

Similar to the create function, updating a record requires protection against users entering a mismatched data type. Status codes should be more specific than just "200" or "500," as this will help you and other developers debug more easily. You can create your own error messages or utilize 400's status codes to indicate client errors:

  • 401 Unauthorized: the requested page needs a username and a password
  • 403 Forbidden: access is forbidden to the requested page
  • 409 Conflict: the request could not be completed because of a conflict

CRUD: Delete

Lastly, the D stands for delete. This action is quite simple to set up and use. However, deleting resources in a relational database can bear indirect consequences. When removing a record from a database, it's a best practice to consider this action permanent and, therefore, to use this operation with caution. SQL doesn't have a command to retrieve deleted records without a backup.

Here, I use the #destroy method to find the appointment by its identification number and then remove it from the database. I chose the no_content status code to indicate that there isn't a payload body for this appointment anymore.

class AppointmentsController < ApplicationController

 def destroy

   @appointment = Appointment.find params[:id]

   @appointment.destroy

   render json: { appointment: @appointment, message: 'Successfully deleted' }, status: :no_content

 end

end

Conventions

For the delete function, you would use the same command for both front end and back end: "DELETE."

Security

As I mentioned previously, consider the delete function a permanent action. Authentication and authorization are critical to protecting resources, so use these security measures generously. Deleting a medication from Shmealth's database would also affect the records of patients to whom that medication was prescribed. Make sure to update related resources as well, especially if the resource being deleted belongs_to another resource.

Conclusion to CRUD API

The term API is so much more than an acronym. APIs allow two or more applications to interact with each other and exchange data. Each of the four CRUD actions has a very specific role, and CRUD APIs wouldn't be complete without all of them.

Thank you for reading! Now you know what a CRUD API is and how to apply each operation to a company's back end in order to protect and interact with records securely.

This post was written by Anika Bernstein. Anika is a full-stack software engineer with a background in environmental studies and data analytics. She specializes in Javascript's React framework, writing clean, readable, and reusable code to help companies improve their website's SEO.

Download Blog Post

The Inside Trace

Subscribe for expert insights on application security.

Thanks! Your subscription has been recorded.

or subscribe to our RSS Feed

Read more

See Traceable in Action

Learn how to elevate your API security today.