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
The following are the the core integration elements for Angular with Rails:
Alternatives to the Rails / Angular approach:
The back-end could be provides through GraphQL with Falcor supporting.
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]; }
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.