What is a CRUD API?
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.
The Inside Trace
Subscribe for expert insights on application security.