Ari
Ari

Reputation: 27

Getting data and post it from and to External API Rails

I was success to get data from external API in rails,

# products_controller.rb
def load_detail_product
  @response = HTTParty.get("http://localhost:3000/api/v1/products/#{params[:id]}",:headers =>{'Content-Type' => 'application/json'})
  @detail_products = @response.parsed_response
  @product = @detail_products['data']['product']
  @product.each do |p|
      @product_id = p['id']
  end
end

In view, when I want create update form I just do like this

<%= link_to 'Edit', edit_product_path(@product_id) %>

but when I call it to _form.html.erb Im geing this error

undefined method `model_name' for #<Hash:0x00007ffb71715cb8> 

# _form.html.erb
<%= form_for @product, authenticity_token: false do |form| %>
    <div class="field">
      <%= f.label :name %>
      <%= f.text_field :name %>
    </div>

    <div class="actions">
      <%= f.submit %>
    </div>

<% end %>

how I get data from external API and put it on form_for and update the data?

my response API

# localhost:3000/api/v1/products/1
{
    "messages": "Product Loaded Successfully",
    "is_success": true,
    "data": {
        "product": [
            {
                "id": 1,
                "name": "Chair",
                "price": "200000",
                "created_at": "2022-03-22T09:24:40.796Z",
                "updated_at": "2022-03-22T09:24:40.796Z"
            }
        ]
    }
}

# for update PUT localhost:3000/api/v1/products/1
{
    "product":
    {
        "name":"Chair",
        "price":20000
    }
}

Upvotes: 0

Views: 2439

Answers (1)

max
max

Reputation: 102222

The main problem here is that you're not creating an abstraction layer between your application and the outside collaborator. By performing a HTTP query directly from your controller and passing the results straight to the view you're making a strong coupling between your application and the API and any changes to the collaborator can break large portions of your application. This creates a fat controller and pushes all the complexity of dealing with the API responses down into the view which both are very bad things.

I would start by actually creating a model that represents a the data in your application:

# app/models/product.rb
class Product
  include ActiveModel::Model
  include ActiveModel::Attributes
  attribute :id
  attribute :name
  # ...

  def persisted?
    true
  end
end

Note that it doesn't inherit from ApplicationRecord - this is a model thats not backed by a database table and instead just uses the API's that rails provide to make it quack like a model - this means it will work right out the box with forms:

<%= form_with(model: @product) do |f| %>
  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

The persisted? method tells the helpers that we updating a model and that it should route to product_path(id) and use PATCH.

But you also need to move the HTTP call out of the controller into a separate class:

# app/clients/products_client.rb
class ProductsClient
  include HTTParty
  base_url "http://localhost:3000/api/v1/products/"
  format :json
  attr_reader :response

  # Get a product from the remote API
  # GET /api/v1/products/:id
  def show(id)
    @response = self.class.get(id)
    if @response.success?
      @product = Product.new(product_params) # normalize the API data
    else
      nil # @todo handle 404 errors and other problems 
    end
  end

  # Send a PATCH request to update the product on the remote API
  # PATCH /api/v1/products/:id
  def update(product) 
    @response = self.class.patch(
      product.id,
      body: product.attributes
    )
    # @todo handle errors better
    @response.success?
  end

  private 

  def product_params
    @response['data']['product'].slice("id")
  end
end

This isn't necissarily the only way or even the right way to write this class. The main point is just that you should not be burdoning your controller with more responsibilies. It has tons of jobs already.

This http client is the only component that should be touching the application boundy and have knowledge of the API. It can be tested in isolation and stubbed out when needed.

Your controller then "talks" to the API only though this client:

class ProductsController < ApplicationController
  before_action :set_product, only: [:edit, :update] # ...

  # GET /products/:id/edit
  def edit
  end

  # PATCH /products/:id
  def update
    @product.assign_attributes(product_params)
    if @product.valid? && ProductsClient.new.update(product) 
      redirect_to "/somewhere"
    else
      render :edit
    end  
  end

  private

  def set_product
    @product = ProductsClient.new.get(params[:id])
    # resuse the existing error handling
    raise ActiveRecord::RecordNotFound unless @product 
  end

  def product_params
    params.require(:product)
          .permit(:name) # ...
  end
end

Upvotes: 2

Related Questions