Streamlining System Setup for Single Page Applications (SPA) with Rails Back-End: Tools and Integration Elements
Table of Contents
Introduction
Simplify the system setup for single page applications (SPA) with a Rails back-end.
- Keywords:
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.
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="#f5f5f5", 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="#d63"; fontcolor="#d63"; style="rounded";
ctrl [label="ArticlesController#show", color="#d63"];
ams [label="ArticleSerializer\nattributes :title, :body\nhas_many :comments\nbelongs_to :author", color="#d63"];
adapt [label="JsonApi adapter\nrender json: @article", color="#d63"];
ctrl -> ams -> adapt [color="#d63"];
}
// 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="#369"; fontcolor="#369"; style="rounded";
subgraph cluster_data {
label="data (primary resource)"; labeljust="l";
color="#693"; fontcolor="#693"; style="rounded";
d_type [label="type\n\"articles\"", color="#693"];
d_id [label="id\n\"1\"", color="#693"];
d_attrs [label="attributes\n{ title, body }", color="#693"];
d_rel [label="relationships\n{ author, comments }", color="#693"];
d_links [label="links\n{ self }", color="#693"];
}
subgraph cluster_included {
label="included (sideloaded)"; labeljust="l";
color="#639"; fontcolor="#639"; style="rounded";
i_author [label="people/9\n(author)", color="#639"];
i_comment1 [label="comments/5", color="#639"];
i_comment2 [label="comments/12", color="#639"];
}
subgraph cluster_meta {
label="meta"; labeljust="l";
color="#963"; fontcolor="#963"; style="rounded";
m_total [label="total-count: 42", color="#963"];
}
subgraph cluster_topLinks {
label="links (top-level)"; labeljust="l";
color="#d36"; fontcolor="#d36"; style="rounded";
tl_self [label="self / next / prev", color="#d36"];
}
// relationships -> included via resource linkage
d_rel -> i_author [color="#639", style=dashed, label="linkage"];
d_rel -> i_comment1 [color="#639", style=dashed];
d_rel -> i_comment2 [color="#639", style=dashed];
}
adapt -> d_type [color="#aaa"];
// Client side: AngularJS $resource consumption
subgraph cluster_client {
label="AngularJS client ($resource + angular-jsonapi)"; labeljust="l";
color="#369"; fontcolor="#369"; style="rounded";
res [label="$resource('/articles/:id')", color="#369"];
parse [label="angular-jsonapi\nparse + index by (type,id)", color="#369"];
model [label="Article model\n.author -> Person\n.comments -> [Comment]", color="#369"];
view [label="ng-repeat\n{{ article.author.name }}", color="#369"];
res -> parse -> model -> view [color="#369"];
}
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.
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.
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 fastjsonapi 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.
