HTTP Resource
An HTTP Resource is a resource backed by an HTTP endpoint instead of an Active Record model. Point it at an external API and Avo will list, show, create, update, and delete records through that API — no database table required.
# app/avo/resources/author.rb
class Avo::Resources::Author < Avo::Core::Resources::Http
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
parse_collection: -> { response["results"] },
parse_count: -> { response["meta"]["count"] }
}
def fields
field :id, as: :id
field :display_name
end
endWith only endpoint configured, Avo assumes the response body is the collection (or record) and reads the total count from response["total"]. The parsing options exist to adapt APIs that wrap their payloads differently.
Install the gem
Add avo-http_resource to your Gemfile:
# Gemfile
gem "avo-http_resource", source: "https://packager.dev/avo-hq/"Then run bundle install. The HTTP Resource becomes available as a new resource type.
Generate a resource
Pass the --http flag to the resource generator:
bin/rails generate avo:resource Author --httpParse the API response
Most APIs wrap their data — a results key for the collection, a meta object for counts. Configure the three parsing procs to tell Avo where things live:
parse_collectionreturns the array of records from the index response.parse_recordreturns a single record from the show response.parse_countreturns the total number of records, used for pagination.
# app/avo/resources/author.rb
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
parse_collection: -> { response["results"] },
parse_record: -> { response },
parse_count: -> { response["meta"]["count"] }
}Each proc runs in an Avo::ExecutionContext with access to raw_response (the HTTParty::Response), response (the parsed body), and headers (the response headers), so you can dig into nested structures or inspect status codes as needed.
Pagination
Avo pages the API for you — index requests always include page and per_page query parameters.
Send authentication headers
Provide headers to send credentials or any other header with every request:
# app/avo/resources/author.rb
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
headers: {
"Authorization" => "Bearer #{ENV.fetch("API_KEY")}"
}
}If the headers must be computed at request time — rotating tokens, per-user credentials — pass a proc returning the hash instead.
Map sorting and filtering to query params
If you want Avo's sorting UI (or any UI state) forwarded to the API, build the query string with query_params. The proc has access to controller params, and its result is merged into the request's query string:
# app/avo/resources/author.rb
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
query_params: -> {
if params[:sort_by].present? && params[:sort_direction].present?
{ sort: "#{params[:sort_by]}:#{params[:sort_direction]}" }
else
{}
end
}
}Customize the backing model
Avo generates an ActiveModel class for each HTTP Resource behind the scenes. Use model_class_eval to define extra behavior on it — for example, obfuscating the ID used in URLs:
# app/avo/resources/author.rb
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
model_class_eval: -> {
define_method :to_param do
Base64.encode64(id)
end
}
}Handle API errors
When the API returns an error, raise Avo::HttpError inside any parsing proc:
parse_collection: -> {
raise Avo::HttpError.new response["message"] if response["error"].present?
response["results"]
}The controller rescues the exception and displays the message as a flash error in the UI, so users get meaningful feedback instead of a broken page.
Customize create, update, and destroy
Out of the box, the HTTP controller persists changes through the resource's client — POST to the endpoint on create, PATCH to endpoint/:id on update, and DELETE to endpoint/:id on destroy. The default implementation looks like this:
def save_record
# Perform either a create or update request based on the current controller action
@response = @resource.client.send(action_name, @record)
# Should return true if the operation succeeded, false otherwise
@response.success?
end
def destroy_model
# Perform a DELETE request to remove the record via the external API
@response = @resource.client.delete(@record.id)
endIf your API needs different paths, extra parameters, or conditional logic, override these methods in the resource's controller:
save_recordmust return a boolean indicating whether the operation succeeded.- Inspect
action_name("create"or"update") to tell the two operations apart.
# app/controllers/avo/authors_controller.rb
class Avo::AuthorsController < Avo::Core::Controllers::Http
def save_record
auth_headers = {
"Authorization" => "Bearer #{ENV.fetch("API_KEY")}"
}
response = if action_name == "create"
MyCustomApi.post("/authors", body: @record.as_json, headers: auth_headers)
else
MyCustomApi.patch("/authors/#{@record.id}", body: @record.as_json, headers: auth_headers)
end
response.success?
end
endDebug console
HTTP Resources ship with an interactive debug console for inspecting exactly what your resource sends and receives. Visit <avo_root>/http-resource/debug (e.g. /avo/http-resource/debug), pick a resource and an action (index, show, count, create, update, or delete), and fire the request.
For each run you can inspect:
- the sent URL, query params, and (masked) request headers
- the raw response alongside the parsed result
- the request timing
- the output of your
parse_collection,parse_record, andparse_countblocks
Errors are surfaced inline per stage — a failed request and a failing parse_* block are reported separately — so a broken adapter never crashes the page.
The console is gated behind the avo-http_resource license feature and Avo's developer/admin access, the same gate as Avo's own debug tools. On top of that, it only lists — and only runs against — resources the current user is authorized to access, honoring each resource's authorization policy. If you use Avo's authorization, a user can never reach a resource from the console that their policy would otherwise hide; if you don't configure authorization, only the developer/admin gate applies.
WARNING
The create, update, and delete actions hit your real external API. From the console they require an explicit confirmation before running.
Add it to the sidebar
To surface the console as a tool in the sidebar's Tools section, add a sidebar item partial to your app at app/views/avo/sidebar/items/_http_resource_debugger.html.erb:
<%= render Avo::Sidebar::LinkComponent.new(
label: "HTTP debugger",
path: File.join(avo.root_path, "http-resource", "debug"),
icon: "tabler/outline/api"
) %>JSON API
The same diagnostics are available as JSON for scripting or agent use. POST to <avo_root>/http-resource/debug/run.json with the params resource, probe_action, id, page, limit, query, body, and confirm. Write actions (create, update, delete) require confirm=1 since they hit the real external API.
Full example
Every adapter option at once, against the OpenAlex API:
# app/avo/resources/author.rb
class Avo::Resources::Author < Avo::Core::Resources::Http
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
parse_collection: -> {
raise Avo::HttpError.new response["message"] if response["error"].present?
response["results"]
},
parse_record: -> {
raise Avo::HttpError.new response["message"] if response["error"].present?
response
},
parse_count: -> { response["meta"]["count"] },
model_class_eval: -> {
define_method :to_param do
Base64.encode64(id)
end
},
headers: {
"Authorization" => "Bearer #{ENV.fetch("API_KEY")}"
},
query_params: -> {
if params[:sort_by].present? && params[:sort_direction].present?
{ sort: "#{params[:sort_by]}:#{params[:sort_direction]}" }
else
{}
end
}
}
def fields
field :id, as: :id
field :display_name
field :cited_by_count, name: "Total citations"
field :works_count, name: "Total works"
end
end