Building an XML API with Rails
Step-by-step Tutorial
XML is old news. But maybe you have never used it and now you need to. It happened to me recently. In this article I’ll share what I’ve learned about building an XML API, using a step-by-step tutorial.
Level: beginner
Pre requisites: basic knowledge of Rails
Create a basic CRUD App
Our API must read data from somewhere. So we’ll start by creating a basic CRUD app to hold some data about products and categories that will be read by our API.
Create a new rails project with the name xml-api
:
$ rails new xml-api$ cd xml-api
Now let’s scaffold our app with two models: Product and Category.
The Product will have two attributes: name and quantity and will be associated with one category (belongs_to).
The Category will have only one attribute: name and will be associated with several products (has_many).
So, we’ll have a one (category) to many (products) association.
$ rails generate scaffold Category name:string$ rails generate scaffold Product name:string quantity:integer category:belongs_to
Rails automatically includes belongs_to :category
in Product model, but we must manually add has_many :products
to Category model:
# app/models/category.rbclass Category < ApplicationRecord
has_many :products
end
to create the database let’s run
$ rails db:migrate
and to get some initial data let’s copy this into seeds.rb
# db/seeds.rbCategory.create!([
{name: 'Book'},
{name: 'DVD'},
{name: 'Blu Ray'}
])Product.create!([
{name: 'Dom quixote de La Mancha', quantity: 12, category_id: Category.find_by_name('Book').id},
{name: 'Hamlet', quantity: 3, category_id: Category.find_by_name('Book').id},
{name: 'War and Peace', quantity: 7, category_id: Category.find_by_name('Book').id},
{name: 'Moby Dick', quantity: 14, category_id: Category.find_by_name('Book').id},
{name: 'Forrest Gump', quantity: 16, category_id: Category.find_by_name('DVD').id},
{name: 'Taxi Driver', quantity: 25, category_id: Category.find_by_name('DVD').id},
{name: 'The Godfather', quantity: 21, category_id: Category.find_by_name('DVD').id},
{name: 'Star Wars: The Last Jedi', quantity: 48, category_id: Category.find_by_name('Blu Ray').id},
{name: 'Dunkirk', quantity: 12, category_id: Category.find_by_name('Blu Ray').id},
{name: 'Black Panther', quantity: 21, category_id: Category.find_by_name('Blu Ray').id}
])
and run
$ rails db:seed
now start rails server
$ rails server
and you can now use your browser to navigate to
http://localhost:3000/products
and you should see this:
Let’s quickly do some tweaks to our App so we improve our navigation and views:
Setting root
to /products
:
# config/routes.rbRails.application.routes.draw do
root "products#index"
...
Creating our Menu:
# app/views/layouts/application.html.erb <body>
<p>
<%= link_to 'Products', products_path %>
<%= link_to 'Categories', categories_path %>
</p>
...
Let’s change views to show category name instead of object reference:
# app/views/products/show.html.erb <%= @product.category.name %>
# app/views/products/index.html.erb <td><%= product.category.name %></td>
and use collection_select
on form to have a dropdown selection of categories:
# app/views/products/_form.html.erb<%= form.collection_select :category_id, Category.order(:name),:id,:name %>
You can check this section complete code here.
Now if we go to:
http://localhost:3000/
We should see:
Now that we have an acceptable CRUD app to insert and edit contents, we’ll see how to create an XML api to serve these contents.
Building an XML API
What happens if we make an XML request to our Rails APP? Let’s see…
Try to make a GET request using curl to see how our App responds:
$ curl -H "Accept: application/xml" -H "Content-Type: application/xml" -X GET http://localhost:3000/categories
You should get this error:
You can also use your browser, appending ‘.xml’ to the url:
http://localhost:3000/categories.xml
But it will also return an error:
This is because we must instruct Rails to respond to XML requests and what to do. So in the controller let’s add respond_to method.
We’ll start by configuring Categories controller, and instruct Rails to accept requests in html and xml formats:
# app/controllers/categories_controller.rb def index
@categories = Category.all
respond_to do |format|
format.html
format.xml { render xml: @categories }
end
end
When we do render xml: @categories
rails will automatically call to_xml
method on @categories
. If you want to pass some options you can explicitly call it:
render xml: @categories.to_xml(options)
Returns a string containing an XML representation of its receiver
Let’s make a GET request using curl:
$ curl -H "Accept: application/xml" -H "Content-Type: application/xml" -X GET http://localhost:3000/categories
Now you should get a valid response with something like:
We are getting a reference for the Category object, but we want to access the content.
In many other tutorials that you can find online this example is used to show the object’s content. This is because of the following change that happened with Rails 4 release:
Ruby on Rails 4.0 Release Notes
11 Active Record
11.1 Notable changes
Model.all now returns an ActiveRecord::Relation, rather than an array of records.
So my suggestion is that we convert the object to hash. To do this we can use as_json
method:
Returns a hash representing the model. Some configuration can be passed through options.
In our case we will do this:
format.xml { render xml: @categories.as_json }
Now when we make the request we get this:
We can also pass some options with as_json
method. Let's see some examples.
If we only want to see id and name fields:
format.xml { render xml: @categories.as_json(only: [:id, :name]) }
we can also indicate the fields we do not want to see (eg. id, created_at):
format.xml {render xml: @categories.as_json(except: [:id, :created_at]) }
if we don’t want types
to be shown we can
format.xml { render xml: @categories.as_json(only: [:id, :name], skip_types: true) }
if we want to include the products in each category
format.xml { render xml: @categories.as_json(only: [:id, :name], skip_types: true, root: true, include: :products ) }
and if we only want to see the product name and quantities
format.xml { render xml: @categories.as_json(only: [:id, :name], skip_types: true, root: true, include: {products: {only: [:name, :quantity]}} ) }
In these examples we’ve been using curl, but we could also have used the browser. In this last example we would get:
For the show
method you could do:
# app/controllers/categories_controller.rb def show
respond_to do |format|
format.html
format.xml { render xml: @category.as_json }
end
end
and access it with:
http://localhost:3000/categories/1.xml
You can check this section complete code here.
So far we’ve seen how we can generate XML configuring our controllers. In the next section we’ll see other option.
Configuring an XML view
Based on MVC (Model-View-Controller) architectural pattern, Rails will check for the correspondent View when the controller is called.
If you try to see product with id=1:
http://localhost:3000/products/1.xml
you’ll get an error:
The message ProductsController#show is missing a template for this request format and variant. request.formats: ["application/xml"]...
appears because Rails is looking for a XML view but it doesn't exist.
So, let’s create one. Create a new file show.xml.builder
and paste the following lines:
# app/views/products/show.xml.builderxml.instruct!
xml.product do
xml.name @product.name
xml.quantity(@product.quantity, type: 'integer')
xml.category @product.category.name
end
xml.instruct!
will insert a processing instruction into the XML markup - the XML prolog. (If your browser doesn´t show it, try checking the page source.)
xml.product do
will create the root element with the name <product>
.
The next three lines will create the child elements. Each line defines the element name, value and attributes (optional).
Let’s try again:
http://localhost:3000/products/1.xml
And you should get:
The same way, well get errors if we try to get a list of the products:
http://localhost:3000/products.xml
You should get the same error as above.
So, let’s create an xml view for products#index
. Create a new file index.xml.builder
and paste the following lines:
# app/views/products/index.xml.builderxml.instruct!
xml.products do
@products.each do |product|
xml.product do
xml.name product.name
xml.quantity product.quantity
xml.category product.category.name
end
end
end
Here we are creating a root element <products>
and the iterating through each
product, creating a <product>
element which contains three child elements: <name>
, <quantity>
and <category>
.
If we refresh the browser we can now see:
How can we get a list of products by category? To get it we must create a new controller method, define a new route and create the respective xml view. Let’s do this. Step by step.
Creating the new products_by_category
controller method:
# app/controllers/products_controller.rb def products_by_category
@categories = Category.all
end
Setting up the new route /products_by_category
:
# config/routes.rb get '/products_by_category', to: 'products#products_by_category'
And finally the new XML view:
# app/views/products/products_by_category.xml.builderxml.instruct!
xml.products_by_category do
@categories.each do |category|
xml.category do
xml.name category.name
category.products.each do |product|
xml.product do
xml.name product.name
xml.quantity product.quantity
end
end
end
end
end
Now if we run in our browser:
http://localhost:3000/products_by_category.xml
We’ll get this:
You can check this section complete code here.
Notice that if you’ll be working with complex XML structures you should use namespaces to avoid conflicts.
API documentation
Documenting your API is very important. If people don’t know how to use your API, they don’t use it. So you should make it easy to learn.
There are many tools that can help you. Some examples are Swagger and Postman. I’ve been using the last one (free version) and I can recommend it (I’m not affiliate).
The Postman documentation tool produces semi-automatic documentation that can be private or public and you may even publish it to the internet if you want your users to access it:
You can check here the full documentation for XML-API.
This finishes this introduction on how to create an XML api with Rails. More advance topics include versioning, authentication, and writing to the api.
I hope that you have enjoyed this reading and that it may be useful to you.
You can check (and clone) the complete repository at https://github.com/pjbelo/xml-api
and check the commit history for the different code versions.
Please feel free to contact. I’ll be glad to help if I can :-)
Cheers,
Paulo