What does CRUD (in the programming world) stand for?
- C: Create
- R: Read
- U: Update
- D: Delete
The apps that you will be creating in Mod 2 will make heavy use of these four actions.
Let's use Rails to build an application where we can manage some tasks.
We're going to follow the MVC design pattern, which Rails uses by default, to implement the CRUD actions for our Task Manager app.
Here is a high level image of the MVC design pattern. You will learn in this Intro to MVC lesson later in Mod 2.
In this tutorial we're building an API. APIs are used to provide data to an external system when they request it, and the APIs we'll see will mostly serve up data in the form of JSON. JSON is a popular data format that looks like a Ruby hash.
Throughout the module, we'll talk through some conventions and best practices, but for now - we'd like for you to follow along with this tutorial. We highly recommend not copying and pasting the code in this tutorial. It's to your advantage to type each line of code on your own.
Before creating our new Task Manager app, let's make sure we are all on the same version of Rails. For this tutorial, you will want to be running Rails 7.1.3. To check which version of rails you have installed, run $ rails -v
. If you see any version other than 7.1.3, you will need to follow these instructions to get the correct version installed.
After confirming that you are running the correct version of Rails, we are ready to get started!
To create your Rails app, navigate to your 2module directory and run the following command:
$ rails new task_manager -T -d="postgresql" --api
Let's break down this command to better understand what is happening. rails new
is the command to create a new Rails app - this will create a lot of directories and files; we will explore this file structure in a moment. task_manager
is going to be the name of the directory in which our Rails app lives and is, essentially, the name of our application. -T
tells Rails that we are not going to use its default testing suite. We will be using RSpec as our testing framework. -d="postgresql"
tells Rails that we will be using a Postgresql database; Rails can handle many different databases, but this is the one we will be using in Mod 2. --api
tells rails that we want to build an API instead of an application that also has a frontend.
Now that we have created our rails app, let's cd task_manager
and explore some of the structure that Rails builds out for us.
Taking a look at the files that Rails creates can be a bit overwhelming at first, but don't worry - this tutorial will only touch on a handful of directories! The top level directories that we are concered with are:
- app - This is where we configure Models and Controllers.
- config - Inside this directory, in the
routes.rb
file is where we will tell our Rails app which HTTP requests to respond to. - db - Where our database structure will be set up.
In addition to these directories, we will also be dealing with our Gemfile, which is where we will tell Rails about any other gems we might need to run our app. For our task manager we will be adding just one gem to our Gemfile. Open your gemfile and add pry
to the :development, :test
group - your Gemfile should now look like this:
source "http://rubygems.org"
git_source(:github) { |repo| "http://github.com/#{repo}.git" }
ruby "3.2.2"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.1.3"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use the Puma web server [http://github.com/puma/puma]
gem "puma", "~> 6.0"
# Build JSON APIs with ease [http://github.com/rails/jbuilder]
# gem "jbuilder"
# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"
# Use Kredis to get higher-level data types in Redis [http://github.com/rails/kredis]
# gem "kredis"
# Use Active Model has_secure_password [http://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
# Use Active Storage variants [http://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible
# gem "rack-cors"
group :development, :test do
# See http://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri mingw x64_mingw ]
gem "pry"
end
group :development do
# Use console on exceptions pages [http://github.com/rails/web-console]
gem "web-console"
# Add speed badges [http://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"
# Speed up commands on slow machines / big apps [http://github.com/rails/spring]
# gem "spring"
end
Any time we update our Gemfile, we will need to tell our application to install or update the additional gems. In your terminal, run the command:
$ bundle install
Great - now we can use binding.pry
anywhere in our app to debug as we go!
Before we can see what our new Rails app can do, we need to do some more set up. First, let's create our app's database. Do this from the command line with:
$ rails db:create
You should see some output like this:
Created database 'task_manager_development'
Created database 'task_manager_test'
We have created two databases, a "development" and a "test" database. In this tutorial, we won't be writing any tests so we won't touch our test database. We are working in the "development" environment.
Great, now our database is created! But, we don't have any tables yet.
We could potentially create a tasks table directly from our terminal, but that's no fun, and it makes it pretty difficult to work with other people. Instead, let's create some migrations.
Migrations allow you to evolve your database structure over time. Each migration includes instructions to make some change to the database (e.g. adding a table, adding columns to a table, dropping a table, etc.). One advantage to this approach is that it will allow you to transfer the application to different computers without transferring the whole database. This isn't a big problem for us now, but as your database grows it will be advantageous to be able to transfer the instructions to create the database instead of the database itself.
To create a migration that will send instructions to create a tasks table to our database, run the following command from your terminal:
$ rails generate migration CreateTasks title:string description:string
In this command, we are telling Rails to generate a migration file that will create a tasks table in our database with two columns - title and description. To see the migration that Rails created, open your db/migrate
directory, and you should have a file in there that is called something like db/migrate/20190414173402_create_tasks.rb
. Open that file and you will see the following:
class CreateTasks < ActiveRecord::Migration[7.1]
def change
create_table :tasks do |t|
t.string :title
t.string :description
t.timestamps
end
end
end
So, now we have a migration with some instructions to tell our database to create a tasks table, but how do we actually get the table created? Run the following in your terminal:
$ rails db:migrate
And you should see something like this:
== 20221130062449 CreateTasks: migrating =======================================
-- create_table(:tasks)
-> 0.0052s
== 20221130062449 CreateTasks: migrated (0.0053s) ==============================
Great! How can we verify that worked?
In your terminal, connect to the database that we just created, and see if we can select some information from our tasks table:
$ rails dbconsole
psql (14.2)
Type "help" for help.
task_manager_development=# SELECT * FROM tasks;
id | title | description | created_at | updated_at
----+-------+-------------+------------+------------
(0 rows)
task_manager_development=#
Don't forget the semicolon.
Awesome - we have a database with a table for tasks! In order to test your API, you'll want to add at least two tasks to your database. Review your SQL practice from intermission to learn how to add some tasks.
If you're still stuck, this is a great time to use ChatGPT to help you generate some data. Try asking something like:
How can I write SQL to insert data into this table?
id | title | description | created_at | updated_at
To exit the psql session, enter the command exit
Now that we've set up our database, it's time to get our server up and running and ready for HTTP requests. To do this, run either
rails server
or rails s
You should see something like this:
=> Booting Puma
=> Rails 7.1.3 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.5 (ruby 3.2.2-p18) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 66200
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
Navigate to http://localhost:3000/ and you should see some Rails magic!
Now, let's take a look back at your terminal and walk through what just happened.
Started GET "/" for ::1 at 2022-11-29 18:51:51 -0700
Processing by Rails::WelcomeController#index as HTML
Rendering /Users/mdao/.rbenv/versions/3.2.2/lib/ruby/gems/3.1.0/gems/railties-7.0.5/lib/rails/templates/rails/welcome/index.html.erb
Rendered /Users/mdao/.rbenv/versions/3.2.2/lib/ruby/gems/3.1.0/gems/railties-7.0.5/lib/rails/templates/rails/welcome/index.html.erb (Duration: 1.4ms | Allocations: 635)
Completed 200 OK in 13ms (Views: 4.9ms | ActiveRecord: 0.0ms | Allocations: 5094)
On the first line, you are seeing a snapshot of the HTTP request that was received by our server when we navigated to localhost:3000 - GET "/"
. This is basically telling us that our application received a request for the information that lives at a certain address.
On the last line, we can see a snapshot of the response that our server sent back to the browser Completed 200 OK
. Meaning, we received a request, were able to process that request and succesfully send back a response. The response is what contains the information that allows the browser to render the default page for your Rails app!
Let's update our app to return something other than the default Rails welcome page - remember our goal is to send back JSON data that a frontend application could use.
Here's our goal. When our browser makes a GET request to localhost:3000/api/v1/tasks
, we want our API to return all of the tasks currently in the database. You'll notice that all of our endpoints start with the namespace api
and the version v1
. You will learn more about namespaces and versions later in this Mod.
We want to see something like this:
Try navigating to localhost:3000/api/v1/tasks
in your own browser. You should see an error telling you that No route matches [GET] "/api/v1/tasks"
. Our app received a request that it was not set up to handle, so let's change that!
First we need to update our config/routes.rb
to handle a GET '/api/v1/tasks'
request. Update your routes.rb
file to include the following:
config/routes.rb
Rails.application.routes.draw do
# Define your application routes per the DSL in http://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
get "/api/v1/tasks", to: "api/v1/tasks#index"
end
This line that we added is telling our application that anytime we receive an HTTP GET request for the URI '/api/v1/tasks', we should perform the index action within our tasks controller.
Go back to your browser and refresh the page - you should now be seeing an error telling you that you have a Routing Error
; specifically, that you have an uninitialized constant Api::V1::TasksController
. This is a good thing; at this point, we have told our application which controller action to perform when this request is received, but we haven't created that controller (or the action) yet. So, let's go do that.
Run the following commands in your terminal to create your file tasks_controller.rb
nested inside app/controllers/api/v1
.
$ mkdir -p app/controllers/api/v1
$ touch app/controllers/api/v1/tasks_controller.rb
app/controllers/api/v1/tasks_controller.rb
class Api::V1::TasksController < ApplicationController
def index
render json: {
example_key: {
example_nested_key: "It worked!"
},
example_array_key: [
"Array item 1", "Array item 2"
]
}
end
end
Go back to your browser and refresh the page. You should now see the json data you added show up in your browser! Does this remind you of anything? (hint: Nested collections in Mod 1)
Try changing the keys and values of the data that's returned.
We're getting close! The last thing we need to do is return the tasks stored in our database instead of this random json. Following MVC conventions, we're going to create a Model to help us do this.
In order to follow MVC conventions, we are going to create this Task class in a app/models/task.rb
file. In that file, create a Task class that looks like this:
app/models/task.rb
class Task < ApplicationRecord
end
Why inherit from ApplicationRecord
? This Task class that we are creating is meant to be a very specific and specialized class that we refer to as a Model. The purpose of Models is to create objects based on records that exist in a database. Rails give us some methods that we can use to help in this creation and we inherit those methods from ApplicationRecord
. Some of the methods that we inherit are all
, find
and create
. These methods are inherited even when our class is totally empty. We're going to make use of the all
method first.
Update your tasks controller's index
action to match the following. Rails will pull all of the Tasks from our database and then render json:
will return that data as json.
app/controllers/api/v1/tasks_controller.rb
class Api::V1::TasksController < ApplicationController
def index
render json: Task.all
end
end
Refresh your browser and you should now see when you make a request to http://localhost:3000/api/v1/tasks you are returned all of the tasks as json. Success!
The next endpoint we need to create is a GET
request to http://localhost:3000/api/v1/tasks/:id, which should return the task with the ID specified. For example, a GET
request to http://localhost:3000/api/v1/tasks/2` would return the task with id 2.
If you make the request http://localhost:3000/api/v1/tasks/2 in your browser. You should see the error No route matches [GET] "/api/v1/tasks/2"
. This is because we haven't added this route. Update your routes.rb
file to include the following:
config/routes.rb
Rails.application.routes.draw do
# Define your application routes per the DSL in http://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
get "/api/v1/tasks", to: "api/v1/tasks#index"
get "/api/v1/tasks/:id", to: "api/v1/tasks#show"
end
Now let's add that show route into our tasks controller:
app/controllers/api/v1/tasks_controller.rb
class Api::V1::TasksController < ApplicationController
def index
render json: Task.all
end
def show
end
end
Remember when we talked about how we are inheriting some functionality from ActiveRecord that helps us interact with our database? Well, now's a great place to take advantage of the ActiveRecord method find
, which will retrieve a record from our database based on that record's ID. In this case, we can use find
like this:
app/controllers/api/v1/tasks_controller.rb
def show
render json: Task.find(params[:id])
end
Navigate to https://localhost:3000/api/v1/tasks/1 and you should now see the json for that particular task!
What is this params
that we are passing into the find
method? Let's throw a binding.pry
at the top of our show
action and see what we can find:
def show
binding.pry
render json: Task.find(params[:id])
end
Refresh your browser and take a look at your terminal. In your pry session, call params and see what is returned.
13: def show
=> 14: binding.pry
15: render json: Task.find(params[:id])
16: end
[1] pry(#<TasksController>)> params
=> #<ActionController::Parameters {"controller"=>"tasks", "action"=>"show", "id"=>"2"} permitted: false>
We are getting a params
object that includes an :id
key that matches with the very end of the uri we visited (/api/v1/tasks/2). Looking at our routes, we see that we set up our URI pattern to accept :id
, but when we visited this site, we typed in 2
which is an actual ID that exists in our database. When we need to get some information, like an ID, from our route in the form of parameters, we can include a symbol of the thing we are expecting when we set up our route - in this case, we are expecting an :id
. So, based on how we set up our routes, we can manipulate and dictate what parameters we want; and what information we will need access to in our controllers.
Remember that you will need to exit your pry session to continue interacting with your site!
Open up Postman which you should have installed over intermission.
In Postman, make the same two requests you have been making in your browser.
GET /localhost:3000/api/v1/tasks
GET /localhost:3000/api/v1/tasks/2
While you can make GET requests in your browser, it's often easier to see what's happening in Postman.
Additionally, we're now going to build POST, PUT and DELETE endpoints and we need an additional tool such as Postman to make those types of requests.
Our goal is to support all of the CRUD operations in our API. We have successfully built out the Read portion, now it's time to build a route for Creating a new task.
Let's set up the request in Postman first. To create a new task, you're making a POST
request to the /api/v1/tasks
endpoint and pass in the data for that new task in your request body. Notice how we change the request type to POST
in the top left and how we are now including a request body.
To build out this endpoint, we will follow the same steps as we have been:
- Add the route.
- Update the controller.
config/routes.rb
Rails.application.routes.draw do
# Define your application routes per the DSL in http://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
get "/api/v1/tasks", to: "api/v1/tasks#index"
get "/api/v1/tasks/:id", to: "api/v1/tasks#show"
post "/api/v1/tasks", to: "api/v1/tasks#create"
end
And then in our tasks controller:
app/controllers/api/v1/tasks_controller.rb
class Api::V1::TasksController < ApplicationController
def index
render json: Task.All
end
def show
render json: Task.find(params[:id])
end
def create
render json: Task.create(task_params)
end
private
def task_params
params.require(:task).permit(:title, :description)
end
end
Now that we have our create endpoint set up, let's put a binding.pry
in our tasks controller's create
action so that it looks like this:
app/controllers/api/v1/tasks_controller.rb
def create
binding.pry
render json: Task.create(task_params)
end
Make a POST request to http://localhost:3000/api/v1/tasks in Postman. In our terminal, we should have hit our pry - let's make sure we have access to the information we need to create a new task:
From: /Users/zoepeterson/turing/task_manager/app/controllers/api/v1/tasks_controller.rb:12 Api::V1::TasksController#create:
11: def create
=> 12: binding.pry
13: render json: Task.create(task_params)
14: end
[1] pry(#<Api::V1::TasksController>)> params
=> #<ActionController::Parameters {"title"=>"Feed the cats", "description"=>"Two scoops in the morning.", "controller"=>"api/v1/tasks", "action"=>"create", "task"=>{"title"=>"Feed the cats", "description"=>"Two scoops in the morning."}} permitted: false>
Looks good! We have access to the parameters we need. Notice that we have also added the following private method.
private
def task_params
params.require(:task).permit(:title, :description)
end
This is making use of a Rails feature called strong parameters. Strong params ensures that only permitted parameters are used to create or update an object, which helps to protect your application from malicious users who might try to update attributes that they shouldn't have access to. It also allows us to require certain attributes.
To see this validation in action, try making a request that doesn't include a request body. Your API should return an error message saying "#<ActionController::ParameterMissing: param is missing or the value is empty: task>",
with the error status code 400
.
You will learn more about strong params later in this Mod.
At this point, we have an API that will allow us to fetch a list of tasks, fetch a specific task, and add a task. We have covered the Create and Read portions of CRUD. Now, let's add some functionality to be able to update existing tasks.
We want to support a PATCH request to /api/v1/tasks/:id
where the user sends the fields to update in the request body.
In our config/routes.rb
file, add the following route:
config/routes.rb
patch '/api/v1/tasks/:id', to: "api/v1/tasks#update"
And in our tasks controller, add the following action:
app/controllers/api/v1/tasks_controller.rb
def update
render json: Task.update(params[:id], task_params)
end
We're making use of the same task_params
method as before for validation. And we're making use of the ActiveRecord's update
method to update the object that corresponds with the ID provided in the path.
Now that we have this edit functionality implemented, make sure you have your server running, and play around with sending requests in Postman to update tasks.
As of now, we have the Create, Read, and Update functions done - all we are missing is Delete!
Let's go add that route to our config/routes.rb
:
config/routes.rb
delete '/api/v1/tasks/:id', to: "api/v1/tasks#destroy"
And, with this route, we will need to add a destroy action to our tasks controller:
def destroy
render json: Task.delete(params[:id])
end
Another new method! We have used find
, create
, and update
so far - what is delete
doing? delete
is another method we are inheriting from ActiveRecord that deletes records in our database based on an ID that you send to the method.
Now, we should be able to send a DELETE
request to http://localhost:3000/api/v1/tasks/:id and delete the task for the provided id!
Try making a DELETE
request and then making a GET
request using the same id. You should get an error message similar to "#<ActiveRecord::RecordNotFound: Couldn't find Task with 'id'=6>",
and the status code 404
.
The goal for this tutorial is to walk you through the full process of building an API that supports the fully CRUD actions. We have successfully done that and we could stop here!
But often times we will want to customize our response a little bit more instead of sending back the full object. For us to accomplish this, we are going to use something called a Serializer.
$ mkdir -p app/serializers
$ touch app/serializers/task_serializer.rb
Let's imagine that our frontend doesn't care about the created_at
and updated_at
timestamps. We want to store those in our database for tracking purposes, but we don't want to send them to the frontend.
Add the following serializer which only includes the fields id
, title
, and description
.
app/serializers/task_serializer.rb
class TaskSerializer
def self.format_tasks(tasks)
tasks.map do |task|
{
id: task.id,
title: task.title,
description: task.description
}
end
end
end
Now that we have a serializer that formats our tasks for our json response we can use it in our controller.
app/controllers/api/v1/tasks_controller.rb
def index
tasks = Task.all
render json: TaskSerializer.format_tasks(tasks)
end
Make a GET request to retrieve all tasks again and we should see the new formatting! If you are curious to learn more about serializers, look ahead to the serializers lesson and do a little research.
Congrats! You have finished your first Rails API that can handle full CRUD functionality for a database resource! We can now Create, Read, Update, and Delete tasks! And you are now familiar with the concept of a serializer to customize how your format your Json response.
- Define CRUD.
- Define MVC.
- What two files would you need to create/modify for a Rails application to respond to a GET request to /api/v1/tasks, assuming you have a Task model.
- What are params? Where do they come from?
- What is the purpose of a serializer?