Using JSON API to join a Rails backend to an Angular app

Introduction

I prefer to keep the UI code separate from the Ruby on Rails server. The advantages are that the complexity of using Ruby on Rails to serve up assets disappears and also that development work on the front end can be done without knowing anything about ruby.  More than one app can use the same API, perhaps an Ionic app for mobile and an Angular app for desktop admin.

Its possible to send raw JSON backwards and forwards, but we quickly get into decisions about pagination, sorting and filtering, and the API itself can become a bit inconsistent and expensive to own and maintain.

JSON-API solves this problem, but you need implementations of this standard in both ruby and javascript.  This article gives example code that uses JSON-API to unify the two.

The finished app

To ease the suspense, here are some screenshots of the finished app.   It is part of a larger project to help people build templates for checklists that will be completed using another mobile app.

The UI is Angular 2 and Material design.  The server is a Rails app that uses an engine containing the API.  All the code is available.

List of templates, clicking on one takes you to the form shown next

A checklist on the right is being built from components on the left.

The screenshot shows a signature component being dragged into a checklist template called ‘Inspection of a rented house …’.

The template will be used by an Ionic mobile app to collect the information, photos, descriptions and signatures from inspections in the real world.  The Ionic app can use the same Rails API.

The API

A Rails engine provides three resources, audit_type, audit_type_components and available_component_types. The app does not have any authentication because its Rails Engine and Angular components are meant to be used in larger projects.

The routes file looks like this.

CheckListEngine::Engine.routes.draw do

  namespace :api do
    jsonapi_resources :audit_type_components

    jsonapi_resources :audit_types do
      jsonapi_related_resources :audit_type_components
    end
  end

end

and the routes are

           Prefix Verb URI Pattern        Controller#Action
check_list_engine      /check_list_engine CheckListEngine::Engine

Routes for CheckListEngine::Engine:
              api_audit_type_component_relationships_audit_type GET       /api/audit_type_components/:audit_type_component_id/relationships/audit_type(.:format)               check_list_engine/api/audit_type_components#show_relationship {:relationship=>"audit_type"}
                                                                PUT|PATCH /api/audit_type_components/:audit_type_component_id/relationships/audit_type(.:format)               check_list_engine/api/audit_type_components#update_relationship {:relationship=>"audit_type"}
                                                                DELETE    /api/audit_type_components/:audit_type_component_id/relationships/audit_type(.:format)               check_list_engine/api/audit_type_components#destroy_relationship {:relationship=>"audit_type"}
                            api_audit_type_component_audit_type GET       /api/audit_type_components/:audit_type_component_id/audit_type(.:format)                             check_list_engine/api/audit_types#get_related_resource {:relationship=>"audit_type", :source=>"check_list_engine/api/audit_type_components"}
api_audit_type_component_relationships_available_component_type GET       /api/audit_type_components/:audit_type_component_id/relationships/available_component_type(.:format) check_list_engine/api/audit_type_components#show_relationship {:relationship=>"available_component_type"}
                                                                PUT|PATCH /api/audit_type_components/:audit_type_component_id/relationships/available_component_type(.:format) check_list_engine/api/audit_type_components#update_relationship {:relationship=>"available_component_type"}
                                                                DELETE    /api/audit_type_components/:audit_type_component_id/relationships/available_component_type(.:format) check_list_engine/api/audit_type_components#destroy_relationship {:relationship=>"available_component_type"}
              api_audit_type_component_available_component_type GET       /api/audit_type_components/:audit_type_component_id/available_component_type(.:format)               check_list_engine/api/available_component_types#get_related_resource {:relationship=>"available_component_type", :source=>"check_list_engine/api/audit_type_components"}
                                      api_audit_type_components GET       /api/audit_type_components(.:format)                                                                 check_list_engine/api/audit_type_components#index
                                                                POST      /api/audit_type_components(.:format)                                                                 check_list_engine/api/audit_type_components#create
                                       api_audit_type_component GET       /api/audit_type_components/:id(.:format)                                                             check_list_engine/api/audit_type_components#show
                                                                PATCH     /api/audit_type_components/:id(.:format)                                                             check_list_engine/api/audit_type_components#update
                                                                PUT       /api/audit_type_components/:id(.:format)                                                             check_list_engine/api/audit_type_components#update
                                                                DELETE    /api/audit_type_components/:id(.:format)                                                             check_list_engine/api/audit_type_components#destroy
                                  api_available_component_types GET       /api/available_component_types(.:format)                                                             check_list_engine/api/available_component_types#index
                                                                POST      /api/available_component_types(.:format)                                                             check_list_engine/api/available_component_types#create
                                   api_available_component_type GET       /api/available_component_types/:id(.:format)                                                         check_list_engine/api/available_component_types#show
                                                                PATCH     /api/available_component_types/:id(.:format)                                                         check_list_engine/api/available_component_types#update
                                                                PUT       /api/available_component_types/:id(.:format)                                                         check_list_engine/api/available_component_types#update
                                                                DELETE    /api/available_component_types/:id(.:format)                                                         check_list_engine/api/available_component_types#destroy
                           api_audit_type_audit_type_components GET       /api/audit_types/:audit_type_id/audit_type_components(.:format)                                      check_list_engine/api/audit_type_components#get_related_resources {:relationship=>"audit_type_components", :source=>"check_list_engine/api/audit_types"}
                                                api_audit_types GET       /api/audit_types(.:format)                                                                           check_list_engine/api/audit_types#index
                                                                POST      /api/audit_types(.:format)                                                                           check_list_engine/api/audit_types#create
                                                 api_audit_type GET       /api/audit_types/:id(.:format)                                                                       check_list_engine/api/audit_types#show
                                                                PATCH     /api/audit_types/:id(.:format)                                                                       check_list_engine/api/audit_types#update
                                                                PUT       /api/audit_types/:id(.:format)                                                                       check_list_engine/api/audit_types#update
                                                                DELETE    /api/audit_types/:id(.:format)                                                                       check_list_engine/api/audit_types#destroy

Using JSON API on both sides of the API

JSON API is complicated internally, and very pedantic about the structure of data and relationships.  So its a bad idea to assemble or parse the json with your own code.

There is a great gem called JSONAPI::Resources to simplify the server code,  and a corresponding adaptor for Angular called Angular2 JSON API.   Together they provide slick way to move representations of objects from ruby to javascript and back again, without having to delve into the structure of the JSON itself.

Server Side:  JSON API Resources

This lovely gem lets you define resources for use by the API.  Instead of writing controllers to handle the REST actions, we leave it up to the gem.

The routes.rb file, shown above, defines resources with the jsonapi_resources  method which orchestrates which controller to call.

The controllers are greatly simplified because they just need to inherit from JSONAPI::ResourceController.

module CheckListEngine
  module Api
    class AuditTypesController < JSONAPI::ResourceController
    end
  end
end

JSONAPI::Resources::Matchers is a neat gem that provides spec matchers to test the API,  although at the time of writing, the tests just check the response status and any objects returned in the data.

JSON API Resources takes a huge amount of complexity out of the hands of the developer, but the error messages can be so vague that you may have to dive into the gem’s code to figure out what is wrong with the call.  For instance I ran into a mix up with the use of underscores in the names of routes and keys.  The fix was to explicitly set underscores in the initializer.

JSONAPI.configure do |config|
  config.default_paginator = :paged

  config.top_level_links_include_pagination = true

  config.default_page_size = 10
  config.maximum_page_size = 20

  # Javascript prefers underscore, but hyphen is standard.  :underscored_key, :camelized_key, :dasherized_key, or custom
  config.json_key_format = :underscored_key
  #:underscored_route, :camelized_route, :dasherized_route, or custom
  config.route_format = :underscored_route
end

Client Side: Angular 2 JSON API

Again, this component lets you abstract the models used by the JSON API.  First create a  a Datastore service by extending JsonApiDatastore

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { AuditType } from '../models/audit_type.model';
import { AuditTypeComponent } from '../models/audit_type_component.model';
import { AvailableComponentTypes } from '../models/available_component_type.model';

import { JsonApiDatastoreConfig, JsonApiDatastore, DatastoreConfig } from 'angular2-jsonapi';

const config: DatastoreConfig = {
  /* TODO: put baseUrl in an environment variable */
  baseUrl: 'http://localhost:3000/check_list_engine/api/',
  models: {
    audit_type: AuditType,
    audit_type_components: AuditTypeComponent,
    available_component_types: AvailableComponentTypes
  }
}

@Injectable()
@JsonApiDatastoreConfig(config)
export class Datastore extends JsonApiDatastore {

  constructor(http: Http) {
    super(http);
  }
}

Then define models corresponding to the resources on the server side

import { JsonApiModelConfig, JsonApiModel, Attribute, BelongsTo } from 'angular2-jsonapi';

import { AuditType } from './audit_type.model';

@JsonApiModelConfig({
  type: 'audit_type_components'
})
export class AuditTypeComponent  extends JsonApiModel {

  @Attribute()
  title: string;

  @Attribute()
  help_text: string;

  @Attribute()
  choices: string;

  @Attribute()
  has_image: boolean;

  @Attribute()
  is_mandatory: boolean;

  @Attribute()
  position: string;

  @BelongsTo()
  audit_type: AuditType;
}

In the Angular view controller, you can call the API and wait for the results

import { Component, OnInit } from '@angular/core';

import { JsonApiQueryData } from 'angular2-jsonapi';
import { Datastore } from '../../services/datastore';
import { AuditType } from '../../models/audit_type.model';

@Component({
  selector: 'app-audit-type-list',
  templateUrl: './audit-type-list.component.html',
  styleUrls: ['./audit-type-list.component.css']
})
export class AuditTypeListComponent implements OnInit {
  audit_types: any;
  selectedAuditType: AuditType;

  constructor(private datastore: Datastore) { }

  ngOnInit() {
    this.getAuditTypes();
  }

  onSelect(audit_type: AuditType): void {
    this.selectedAuditType = audit_type;
  }

  getAuditTypes() {
    this.datastore.findAll(AuditType, {
      include: 'audit_type_components'
    }).subscribe(
      (audit_types: JsonApiQueryData<AuditType>) => {
        this.audit_types = audit_types.getModels();
      }
    );
  }
}

In both the ruby and Typescript code,  we’re spared from parsing or building the complicated json relationships and lists of associated objects.

On several occasions I needed to look into the Angular 2 JSON API code to figure out why something was not working for me.  It was always because I was calling the server with the wrong route name, but this was time consuming to understand.

For the drag and drop I used ng2-dragular, which was fun to use.

Development tools

Another advantage of clearly separating the server and client code is that you can use the right development tool for the job.

I use Rubymine to develop the Rails project and WebStorm for the Angular work.    Both of these JetBrains IDEs are really similar to use, but specialised for the languages used.

Angular view code can become verbose, but Angular 2 has a neat way of defining components that can nested.

Each view component is organised into a collection of small files

Material Design proved to be a pain when laying out admin pages.  For Desktop apps I think that Bootstrap is easier to use.

The code is on Github

There are three repositories for this project.

  1. A rails engine to encapsulates the models and controllers
  2. Material Design / Angular 2 app
  3. Rails app which includes both of the above

The rails app is just a shell that mounts the engine and includes the UI code in its public directory.    (Copy the contents of the dist directory of the UI into the public directory of the Rails app).

The UI assumes that Rails will be started on port 3000, this needs to be stored in an environment variable.

I put the server side models into an engine so that it can used in more than one future project.

Installing and running the demo app

The demo app uses Postgres.

git clone [email protected]:guy-roberts/check_list_full_app.git

cd check_list_full_app

bundle install

rake db:setup

Create some seed data

rake check_list_engine:create_audit_data

rails s

Then navigate to localhost:3000, choose a checklist template and drag the components around.  They should be saved to the database.

Rationale for the work

I kept coming across the need for a convenient and cheap way to build checklists that describe how to do a specific task. For instance;

– for an engineer servicing a burglar alarm
– a pest control person visiting a property to remove an infestation
– an estate agent checking on the state of a rental property

Checklists are used to tell the agent where the job is,  who it is for,  what steps to carry out.  The same report can also be used to record the customer’s signature,  any materials used and time starting and stopping work.

Although each of these reports are different, they all have the same component parts

  • Title
  • Address
  • Photo
  • Text description
  • Signature
  • Yes / No box answering a question
  • Choice / Select box
  • Sections

So the admin app provides a way to drag and drop these components into an ordered checklist.

Once a template is ready, it can be used by somebody with another mobile app to carry out the checklist and save the results back to the API.  This is done by another app written in Ionic.  Authentication is by token, not described here.  Hosting is on Heroku.

A basis for other projects

Projects sometimes feel like walks through a forest, if you know what you’re doing then its great, but step off into the under growth and you’re in for a long, scratchy afternoon.

The JSONAPI:Resources gem and the Angular 2 JSON API adapter work together to hide the complexity of JSON API from the server and client code.  They provide ways to paginate, sort and filter resources.

Best of all, they free up time for the developer to spend on designing a cool user interface and talk to the users.

About Guy Roberts