Building an XML API with Rails

Step-by-step Tutorial

Paulo Belo
8 min readDec 16, 2018

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)

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:

as_json(options = nil)

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

--

--

Paulo Belo

Developer. Freelancer. Looking for the right words, mainly in ruby and dart.