SPA Setup with Rails: JSON API and Angular

Table of Contents

1. Introduction

Simplify the system setup for single page applications (SPA) with a Rails back-end.

  • Keywords:

2. Tools

3. Document shape

// JSON:API document shape — server (AMS) -> document -> client ($resource)
digraph jsonapi_shape {
    rankdir=TB;
    graph [bgcolor="white", fontname="Helvetica", fontsize=11,
           pad="0.3", nodesep="0.3", ranksep="0.35"];
    node  [shape=box, style="rounded,filled", fontname="Helvetica",
           fontsize=10, fillcolor="#dbeafe", color="#888"];
    edge  [color="#aaa"];
    // Tailwind palette: #d36 #d63 #693 #369 #639 #963

    // Server side: Rails + ActiveModelSerializer
    subgraph cluster_server {
        label="Rails server (ActiveModelSerializer 0.10)"; labeljust="l";
        color="#b45309"; fontcolor="#b45309"; style="rounded";
        ctrl  [label="ArticlesController#show",          color="#b45309"];
        ams   [label="ArticleSerializer\nattributes :title, :body\nhas_many :comments\nbelongs_to :author", color="#b45309"];
        adapt [label="JsonApi adapter\nrender json: @article", color="#b45309"];
        ctrl -> ams -> adapt [color="#b45309"];
    }

    // The document itself (JSON:API 1.0 top-level members)
    subgraph cluster_doc {
        label="JSON:API response document (Content-Type: application/vnd.api+json)";
        labeljust="l"; color="#1d4ed8"; fontcolor="#1d4ed8"; style="rounded";

        subgraph cluster_data {
            label="data (primary resource)"; labeljust="l";
            color="#15803d"; fontcolor="#15803d"; style="rounded";
            d_type  [label="type\n\"articles\"",                color="#15803d"];
            d_id    [label="id\n\"1\"",                          color="#15803d"];
            d_attrs [label="attributes\n{ title, body }",        color="#15803d"];
            d_rel   [label="relationships\n{ author, comments }", color="#15803d"];
            d_links [label="links\n{ self }",                    color="#15803d"];
        }

        subgraph cluster_included {
            label="included (sideloaded)"; labeljust="l";
            color="#6b21a8"; fontcolor="#6b21a8"; style="rounded";
            i_author   [label="people/9\n(author)",   color="#6b21a8"];
            i_comment1 [label="comments/5",            color="#6b21a8"];
            i_comment2 [label="comments/12",           color="#6b21a8"];
        }

        subgraph cluster_meta {
            label="meta"; labeljust="l";
            color="#a16207"; fontcolor="#a16207"; style="rounded";
            m_total [label="total-count: 42",          color="#a16207"];
        }

        subgraph cluster_topLinks {
            label="links (top-level)"; labeljust="l";
            color="#b91c1c"; fontcolor="#b91c1c"; style="rounded";
            tl_self [label="self / next / prev",       color="#b91c1c"];
        }

        // relationships -> included via resource linkage
        d_rel -> i_author   [color="#6b21a8", style=dashed, label="linkage"];
        d_rel -> i_comment1 [color="#6b21a8", style=dashed];
        d_rel -> i_comment2 [color="#6b21a8", style=dashed];
    }

    adapt -> d_type [color="#aaa"];

    // Client side: AngularJS $resource consumption
    subgraph cluster_client {
        label="AngularJS client ($resource + angular-jsonapi)"; labeljust="l";
        color="#1d4ed8"; fontcolor="#1d4ed8"; style="rounded";
        res    [label="$resource('/articles/:id')",    color="#1d4ed8"];
        parse  [label="angular-jsonapi\nparse + index by (type,id)", color="#1d4ed8"];
        model  [label="Article model\n.author -> Person\n.comments -> [Comment]", color="#1d4ed8"];
        view   [label="ng-repeat\n{{ article.author.name }}", color="#1d4ed8"];
        res -> parse -> model -> view [color="#1d4ed8"];
    }

    d_type    -> parse [color="#aaa"];
    i_author  -> parse [color="#aaa", style=dotted];
    i_comment1 -> parse [color="#aaa", style=dotted];
    i_comment2 -> parse [color="#aaa", style=dotted];
}

diagram-jsonapi-shape.png

The two cluster colors that matter: data (green, #693) is the primary resource the request asked for; included (purple, #639) is everything the server sideloaded so the client avoids N+1 round-trips. The dashed linkage edges are how relationships in data point at entries in included via (type, id) tuples — that pairing is the entire point of the format.

4. Related notes

  • GraphQL — the alternative that ate JSON:API's lunch for typed client-server contracts; same problem (avoid N+1, let the client shape responses), different solution.
  • dataLayer schema — another exercise in nailing down a JSON envelope shape so producers and consumers agree without runtime surprises.
  • Design-driven APIs — JSON:API is one of several spec-first formats (OpenAPI, AsyncAPI, GraphQL SDL) where the contract is the artifact teams ship.
  • jq — when you receive a JSON:API document on the command line, jq '.included[] | select(.type="people")'= is how you pull sideloaded resources out for inspection.

5. Postscript (2026)

Most of the 2015 stack here is gone. AngularJS reached end-of-life on December 31, 2021 and Google stopped shipping security patches; any surviving $resource code is on a frozen runtime. jakubrohleder/angular-jsonapi hasn't seen a release since 2017.

JSON:API itself shipped 1.1 in September 2022, adding a profile mechanism (profile media-type parameter), the @-Members notation for extensions, and a couple of clarifications to how relationship linkage serializes. It is still maintained but no longer the default choice for new Rails APIs. ActiveModelSerializers was effectively unmaintained by 2019; Netflix's fast_jsonapi forked into the community-maintained jsonapi-serializer gem (2020) and that is what current Rails codebases use when they still want JSON:API output.

The broader API-spec race went to OpenAPI. OpenAPI 3.1 (February 2021) aligned with JSON Schema 2020-12 and is now the default for both contract-first design and code-generation in Rails (rswag) and beyond. HATEOAS — the part of REST that links and relationships were trying to operationalize — quietly faded; almost no production client follows hypermedia links at runtime.

The niche JSON:API was strongest in (typed CRUD over a relational back-end with sideloading) is now split between two newer tools: GraphQL when teams want a query language and federation, and tRPC (2020+) when the front-end and back-end are both TypeScript and the team would rather skip the schema language entirely. For a fresh Rails + SPA project in 2026, the realistic choices are OpenAPI 3.1 + generated clients, GraphQL via graphql-ruby, or — if the front-end is also Rails (Hotwire/Turbo) — skipping the JSON envelope discussion altogether.