DEV Community

Josiah
Josiah

Posted on • Edited on

Active Storage. Storing Files With a Rails API

Are you in the quite specific scenario of wanting to upload photos to a rails backend with a react front end but don't know how? I've got you covered with a step by step guide on working with active storage to store your audio, video, and photo files.

Let me just start this by saying that this tutorial may not cover complete best practice as I have been crunched on time and needed the quickest solutions to my problem. This blog is meant to help those of us who are newer to the world of code. With that out of the way, let's get started.

Firstly, I'm going to be using a rails api with a postgresql database and a react front end. I'll do this with the command
rails new project_name --database=postgresql --api

From inside the directory of your new project you can run your vite, create-react-app, or whatever other command you will be using to build your client side. Once your API has been built along with your client side starter, we can move on.

Now, you will want to ensure you have all your gems installed. First, uncomment the rack-cors gem, we'll get to why this is necessary later. Next we will need a gem that is not automatically given to us by ruby in newer versions, this is called active model serializers, be sure to add it to your gem file.

gem "rack-cors"
gem "active_model_serializers", "~> 0.10.13"
Enter fullscreen mode Exit fullscreen mode

Then run bundle install to ensure you have your gems installed. Make sure you have done this before you start your migrations or rails won't generate the serializer for each table you make(we need those).

From here you are going to want to create your database with rails db:create

Then install the rails storage method active storage. This can be done by running the command rails active_storage:install

This will add two migrations to your table. Now is where you would add the rest of your project tables, in this example case, a table for my user that will later have a profile photo. This table will have a name for the user to which we will add a picture later. I'll add this table to my database with the command rails g scaffold User name. From here we are good to migrate with. rails db:migrate.

Once you have migrated your tables here is where we will want to jump into the model(app/models/user.rb) for our user table and we will want to add the has_one_attached macro given to us by active storage.

class User < ApplicationRecord

   has_one_attached :image

   end
Enter fullscreen mode Exit fullscreen mode

In this scenario, image can be named whatever you would like it to be.

Next, we will hop over to our serializer for our user table(app/serializers/user_serializer.rb) and add our image attributes that we want in our table. The first step to this is including our rails app route url helper. This will help generate a url that links to our image. The first step is done like so

class UserSerializer < ActiveModel::Serializer
  include Rails.application.routes.url_helpers
  attributes :id, :name

end
Enter fullscreen mode Exit fullscreen mode

This should automatically generate with the attributes you created when you started. Next we will want to define the route to the image, this can be done by creating a method for defining the file you want to return and the method name should match the name given when we defined the has_one_attached macro. After this method has been defined, we add the method name as an attribute.

class UserSerializer < ActiveModel::Serializer
  include Rails.application.routes.url_helpers
  attributes :id, :name, :image

  def image
  rails_blob_path(object.image, only_path: true) if object.image.attached?
  end
end
Enter fullscreen mode Exit fullscreen mode

Just a few more steps before we move to front end. We need to change the params.permit in our user controller so head on over to app/controllers/user_controller and from there find the user_params method. That should look something like

def user_params
      params.require(:user).permit(:name)
    end
Enter fullscreen mode Exit fullscreen mode

What we need to do is add the image attribute that we added up above to our params like so...

def user_params
      params.require(:user).permit(:name, :image)
    end
Enter fullscreen mode Exit fullscreen mode

Let's finish up the work on the backend by enabling cors. We can do this by going to the config/initializers/cors.rb file. That file should look something like this...

# config/initializers/cors.rb
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

# Rails.application.config.middleware.insert_before 0, Rack::Cors do
#   allow do
#     origins 'example.com'
#
#     resource '*',
#       headers: :any,
#       methods: [:get, :post, :put, :patch, :delete, :options, :head]
#   end
# end
Enter fullscreen mode Exit fullscreen mode

In that file, you will want to uncomment the 7th line(the one that starts with Rails.application) all the way down to the bottom of the file. Then you will want to change the 10th line. In the quotes where it says 'example.com', you will want to put your domain, commonly 'localhost:3000', alternatively, you can put a *, but this is not recommended. The file should look something like this

# config/initializers/cors.rb
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
   allow do
     origins 'example.com'

     resource '*',
       headers: :any,
       methods: [:get, :post, :put, :patch, :delete, :options, :head]
   end
 end
Enter fullscreen mode Exit fullscreen mode

After all of that, we have finished the backend configuration! Let's move to React now.

For react, we will need to create our form to input our necessary fields. This can be done by binding the FormData constructor to the form element or it can be done using state. We only have a couple necessary elements so I'll go that route here. We'll first build our states, then build our return statement where we set our states. We'll also add an onSubmit to the form with a function that we'll build later.

const [name, setName]=useState(null)
const [image, setImage]=useState(null)

return( 
    <form onSubmit={handleSubmit}>
      <input type="text" 
      onChange={(e)=>setName(e.target.value)}/>
      <input type="file" 
      onChange={(e)=>setImage(e.target.files[0])}/>
    </form>
)
Enter fullscreen mode Exit fullscreen mode

Next lets build that handleSubmit function. To submit forms with active storage, we'll need a JavaScript constructor called FormData. FormData is required for sending files from a form. To do this we must first declare the constructor as a constant that we can call whatever we want, I'll call it formData. We can then append the necessary items to this FormData constructor. This append statement takes two arguments, the first being the key in the object you are wanting to send, and the second, the value you want to send. We can add this to a submit event and test what it outputs.

function handleSubmit(){
const formData = new FormData()
formData.append("name", name)
formData.append("image", image)
}
Enter fullscreen mode Exit fullscreen mode

After appending the necessary data to the form, we'll need to send our data to the backend. We'll build our post statement with formData as our body.

function handleSubmit(){
const formData = new FormData()
formData.append("name", name)
formData.append("image", image)

fetch("api/users", {
method: "POST",
body: formData
}).then({/*do something with your data here*/})
}
Enter fullscreen mode Exit fullscreen mode

There you have it, active record used simply with react! Leave any improvements or comments below so we can all program better!

Top comments (12)

Collapse
 
fabianpinop profile image
Fabian Pino • Edited

I followed all the steps but I get the following error please help
ActionDispatch::Http::Parameters::ParseError (767: unexpected token at '------WebKitFormBoundaryxaYAnLtlkajqzq9E

Collapse
 
prajaychaudhary profile image
Prajay Chaudhary

Did you able to solve that problem?

Collapse
 
fabianpinop profile image
Fabian Pino

Yes it's already solved I forgot to leave a comment

Thread Thread
 
prajaychaudhary profile image
Prajay Chaudhary

How did you solve that?. Can I have the repo please?.

Thread Thread
 
fabianpinop profile image
Fabian Pino

in my case the problem was in the headers I had not noticed that content type application json was being sent basically it is not necessary to send something in the header unless you need token or something similar since when sending the form data it reads it as multipart

Thread Thread
 
fabianpinop profile image
Fabian Pino

I'm sorry but the repository is private to the company where I work

Thread Thread
 
prajaychaudhary profile image
Prajay Chaudhary • Edited

After making post request from my frontend I am getting SystemStackError (stack level too deep):

Thread Thread
 
fabianpinop profile image
Fabian Pino

Could you share the code snippet in which you create the object and make the request?

Thread Thread
 
prajaychaudhary profile image
Prajay Chaudhary

Can we talk somewhere privately so that I can share you everything?

Thread Thread
 
prajaychaudhary profile image
Prajay Chaudhary

Started POST "/requests" for ::1 at 2023-06-03 12:37:57 +0200
ActiveRecord::SchemaMigration Pluck (0.1ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by RequestsController#create as /
Parameters: {"request"=>{"owner_id"=>"14", "address"=>"Sioux Falls Regional Airport, North Jaycee Lane, Sioux Falls, SD, USA", "title"=>"ede", "description"=>"ededed", "request_type"=>"One Time Help", "image"=>#, @original_filename="aboutPic.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"request[image]\"; filename=\"aboutPic.jpg\"\r\nContent-Type: image/jpeg\r\n">}}
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 14], ["LIMIT", 1]]
TRANSACTION (0.2ms) begin transaction
↳ app/controllers/requests_controller.rb:39:in create'
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 14], ["LIMIT", 1]]
↳ app/controllers/requests_controller.rb:39:in
create'
Request Create (3.0ms) INSERT INTO "requests" ("request_type", "request_status", "title", "description", "address", "longitude", "latitude", "created_at", "updated_at", "image", "owner_id") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["request_type", "One Time Help"], ["request_status", nil], ["title", "ede"], ["description", "ededed"], ["address", "Sioux Falls Regional Airport, 2801 N Jaycee Ln, Sioux Falls, SD 57104, USA"], ["longitude", -96.7402171], ["latitude", 43.5828065], ["created_at", "2023-06-03 10:37:58.226690"], ["updated_at", "2023-06-03 10:37:58.226690"], ["image", nil], ["owner_id", 14]]
↳ app/controllers/requests_controller.rb:39:in create'
ActiveStorage::Attachment Load (0.8ms) SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ? [["record_id", 65], ["record_type", "Request"], ["name", "image"], ["LIMIT", 1]]
↳ app/controllers/requests_controller.rb:39:in
create'
ActiveStorage::Blob Create (0.5ms) INSERT INTO "active_storage_blobs" ("key", "filename", "content_type", "metadata", "service_name", "byte_size", "checksum", "created_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?) [["key", "u894h1xx89cd5uainuz6sov5elg2"], ["filename", "aboutPic.jpg"], ["content_type", "image/png"], ["metadata", "{\"identified\":true}"], ["service_name", "local"], ["byte_size", 160361], ["checksum", "K9W6jdq1ahsDFH5hD9ID4A=="], ["created_at", "2023-06-03 10:37:58.241950"]]
↳ app/controllers/requests_controller.rb:39:in create'
ActiveStorage::Attachment Create (0.3ms) INSERT INTO "active_storage_attachments" ("name", "record_type", "record_id", "blob_id", "created_at") VALUES (?, ?, ?, ?, ?) [["name", "image"], ["record_type", "Request"], ["record_id", 65], ["blob_id", 23], ["created_at", "2023-06-03 10:37:58.245722"]]
↳ app/controllers/requests_controller.rb:39:in
create'
Request Update (0.5ms) UPDATE "requests" SET "updated_at" = ? WHERE "requests"."id" = ? [["updated_at", "2023-06-03 10:37:58.249002"], ["id", 65]]
↳ app/controllers/requests_controller.rb:39:in create'
TRANSACTION (0.9ms) commit transaction
↳ app/controllers/requests_controller.rb:39:in
create'
Disk Storage (2.0ms) Uploaded file to key: u894h1xx89cd5uainuz6sov5elg2 (checksum: K9W6jdq1ahsDFH5hD9ID4A==)
[ActiveJob] Enqueued ActiveStorage::AnalyzeJob (Job ID: 29b97ba1-7686-457b-9472-3bf3f5c5eeca) to Async(default) with arguments: #>
Completed 500 Internal Server Error in 442ms (ActiveRecord: 7.3ms | Allocations: 63482)

SystemStackError (stack level too deep):

app/controllers/requests_controller.rb:40:in `create'
[ActiveJob] [ActiveStorage::AnalyzeJob] [29b97ba1-7686-457b-9472-3bf3f5c5eeca] ActiveStorage::Blob Load (0.1ms) SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ? [["id", 23], ["LIMIT", 1]]
[ActiveJob] [ActiveStorage::AnalyzeJob] [29b97ba1-7686-457b-9472-3bf3f5c5eeca] Performing ActiveStorage::AnalyzeJob (Job ID: 29b97ba1-7686-457b-9472-3bf3f5c5eeca) from Async(default) enqueued at 2023-06-03T10:37:58Z with arguments: #>
[ActiveJob] [ActiveStorage::AnalyzeJob] [29b97ba1-7686-457b-9472-3bf3f5c5eeca] Skipping image analysis because the ruby-vips gem isn't installed
[ActiveJob] [ActiveStorage::AnalyzeJob] [29b97ba1-7686-457b-9472-3bf3f5c5eeca] TRANSACTION (0.1ms) begin transaction
[ActiveJob] [ActiveStorage::AnalyzeJob] [29b97ba1-7686-457b-9472-3bf3f5c5eeca] ActiveStorage::Blob Update (0.4ms) UPDATE "active_storage_blobs" SET "metadata" = ? WHERE "active_storage_blobs"."id" = ? [["metadata", "{\"identified\":true,\"analyzed\":true}"], ["id", 23]]
[ActiveJob] [ActiveStorage::AnalyzeJob] [29b97ba1-7686-457b-9472-3bf3f5c5eeca] TRANSACTION (0.6ms) commit transaction
[ActiveJob] [ActiveStorage::AnalyzeJob] [29b97ba1-7686-457b-9472-3bf3f5c5eeca] Performed ActiveStorage::AnalyzeJob (Job ID: 29b97ba1-7686-457b-9472-3bf3f5c5eeca) from Async(default) in 120.74ms

Thread Thread
 
fabianpinop profile image
Fabian Pino

Of course, I'll leave you my email f.pino.perez.dev@gmail.com, I don't think a live conversation will be fruitful since my English is horrible xd

Thread Thread
 
prajaychaudhary profile image
Prajay Chaudhary

i have just sent you an email, please do check there.