Overview
Single Page Applications (SPAs) represent a web application architecture where the client loads a single HTML document and dynamically updates the page content without navigating to new pages. Unlike traditional multi-page applications that request new HTML from the server for each navigation, SPAs load all necessary code (HTML, CSS, JavaScript) with the initial page load or dynamically fetch resources as needed.
The SPA pattern emerged from the need to create more responsive, desktop-like web experiences. Traditional web applications suffer from latency during page transitions, where each navigation requires a full page reload, re-rendering the entire layout, and re-establishing application state. SPAs eliminate this latency by handling routing and rendering client-side, requesting only data from the server.
Modern SPAs typically follow a client-server architecture where the server exposes a RESTful or GraphQL API, and the client consumes this API to fetch and update data. The browser's History API enables SPAs to update the URL without triggering page reloads, maintaining the expected browser behavior of back/forward navigation and bookmarkable URLs.
The architecture shifts significant processing responsibility from the server to the client. The server becomes primarily a data API provider, while the client handles rendering, routing, state management, and user interaction logic. This separation enables independent scaling and deployment of frontend and backend systems.
Traditional Multi-Page Application:
User clicks link → Server renders new HTML → Browser loads entire page
Single Page Application:
User clicks link → Client updates route → Client fetches data via API → Client updates DOM
The SPA pattern has become dominant in modern web development, particularly for applications requiring rich interactivity, real-time updates, or complex user interfaces. Applications like Gmail, Trello, Slack, and Twitter use SPA architectures to deliver responsive experiences.
Key Principles
Client-Side Routing: SPAs implement routing in JavaScript, intercepting navigation events and updating the displayed content without requesting new HTML documents. The router maps URL patterns to components or views, handling both user-initiated navigation and programmatic route changes. Modern browsers provide the History API (pushState, replaceState, popstate events) enabling SPAs to manipulate the URL and browser history without page reloads.
Dynamic Content Loading: Instead of receiving rendered HTML from the server, SPAs fetch data asynchronously through AJAX requests and render content client-side. The initial page load includes the application shell—the minimal HTML structure and JavaScript required to bootstrap the application. Subsequent interactions trigger API requests that return JSON data, which the client transforms into DOM elements.
State Management: SPAs maintain application state in memory throughout the session. Navigation between views preserves state without server round-trips. This requires explicit state management patterns, as the application must track current user context, loaded data, UI state, and form values across navigation events. State management complexity increases with application size, leading to frameworks like Redux, MobX, and Vuex.
Declarative Rendering: SPAs typically use declarative rendering approaches where components describe what the UI should look like for a given state, and the framework handles updating the DOM efficiently. This differs from imperative DOM manipulation where code explicitly specifies how to change elements. Declarative rendering simplifies reasoning about UI behavior and enables optimization techniques like virtual DOM diffing.
Component Architecture: SPAs organize UI into reusable components with encapsulated logic and styling. Components form hierarchies where parent components pass data to children through props or attributes, and children communicate with parents through events or callbacks. This architecture promotes code reuse, testability, and separation of concerns.
API-Driven Data Access: SPAs treat the backend as a service layer accessed through HTTP APIs. The client requests data in structured formats (JSON, GraphQL responses), decoupled from presentation concerns. This enables frontend and backend development to proceed independently and allows multiple clients (web, mobile, desktop) to share the same API.
Ruby Implementation
Ruby backends serve SPAs by providing the API layer and serving static assets. The server handles data persistence, business logic, authentication, and authorization, while delegating presentation to the client.
Serving the SPA Shell: Ruby web frameworks serve the initial HTML page that bootstraps the SPA. This requires configuring routes to serve the index.html for all SPA routes and avoid 404 errors on client-side routes.
# Rails: Serve SPA for all routes
class ApplicationController < ActionController::Base
def index
render file: Rails.public_path.join('index.html'), layout: false
end
end
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :posts
resources :comments
end
end
# Catch-all route for SPA (must be last)
get '*path', to: 'application#index', constraints: ->(req) do
!req.xhr? && req.format.html?
end
root 'application#index'
end
Building JSON APIs: Rails API mode provides a streamlined framework for building JSON APIs without view rendering overhead. The API returns structured data that the SPA client consumes.
# app/controllers/api/v1/posts_controller.rb
module Api
module V1
class PostsController < ApplicationController
def index
posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
render json: {
posts: posts.map { |post| post_json(post) },
meta: {
current_page: posts.current_page,
total_pages: posts.total_pages,
total_count: posts.total_count
}
}
end
def show
post = Post.find(params[:id])
render json: { post: post_json(post, include_content: true) }
end
def create
post = current_user.posts.build(post_params)
if post.save
render json: { post: post_json(post) }, status: :created
else
render json: { errors: post.errors }, status: :unprocessable_entity
end
end
private
def post_json(post, include_content: false)
{
id: post.id,
title: post.title,
summary: post.summary,
author: {
id: post.author.id,
name: post.author.name
},
created_at: post.created_at.iso8601,
updated_at: post.updated_at.iso8601
}.tap do |json|
json[:content] = post.content if include_content
end
end
def post_params
params.require(:post).permit(:title, :content, :summary)
end
end
end
end
Authentication with Token-Based Auth: SPAs cannot rely on traditional cookie-based sessions alone due to CORS and the stateless nature of API requests. Token-based authentication (JWT, OAuth tokens) provides a stateless authentication mechanism.
# app/controllers/api/v1/sessions_controller.rb
module Api
module V1
class SessionsController < ApplicationController
skip_before_action :authenticate_user!, only: [:create]
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token = JsonWebToken.encode(user_id: user.id)
render json: {
token: token,
user: {
id: user.id,
email: user.email,
name: user.name
}
}
else
render json: { error: 'Invalid credentials' }, status: :unauthorized
end
end
def destroy
# Token invalidation could happen here if using a token blacklist
head :no_content
end
end
end
end
# lib/json_web_token.rb
class JsonWebToken
SECRET_KEY = Rails.application.credentials.secret_key_base
def self.encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET_KEY)
end
def self.decode(token)
body = JWT.decode(token, SECRET_KEY)[0]
HashWithIndifferentAccess.new(body)
rescue JWT::DecodeError, JWT::ExpiredSignature
nil
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
before_action :authenticate_user!
private
def authenticate_user!
header = request.headers['Authorization']
token = header.split(' ').last if header
decoded = JsonWebToken.decode(token)
@current_user = User.find(decoded[:user_id]) if decoded
rescue ActiveRecord::RecordNotFound
render json: { error: 'Unauthorized' }, status: :unauthorized
end
def current_user
@current_user
end
end
CORS Configuration: SPAs running on different domains than the API require CORS headers to make cross-origin requests. Rails provides the rack-cors gem for CORS configuration.
# Gemfile
gem 'rack-cors'
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins ENV.fetch('CORS_ORIGINS', 'http://localhost:3000').split(',')
resource '/api/*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true,
max_age: 86400
end
end
WebSocket Support for Real-Time Features: SPAs requiring real-time updates (chat, notifications, live data) use WebSockets. Rails Action Cable provides WebSocket integration.
# app/channels/notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
def unsubscribed
stop_all_streams
end
end
# Broadcasting notifications from the backend
NotificationsChannel.broadcast_to(
user,
{
type: 'new_message',
message: {
id: message.id,
content: message.content,
sender: message.sender.name,
created_at: message.created_at.iso8601
}
}
)
Asset Pipeline for SPA Build Artifacts: Rails serves SPA build artifacts (bundled JavaScript, CSS, images) from the public directory. Modern SPAs use build tools (Webpack, Vite, esbuild) that output production-optimized bundles.
# config/environments/production.rb
Rails.application.configure do
# Serve static files from public directory
config.public_file_server.enabled = true
# Enable gzip compression
config.middleware.use Rack::Deflater
# Set far-future expires headers for fingerprinted assets
config.public_file_server.headers = {
'Cache-Control' => 'public, max-age=31536000, immutable'
}
end
Design Considerations
When SPAs Make Sense: SPAs excel in applications requiring rich interactivity, complex state management, or frequent user interactions without page reloads. Dashboards, admin panels, project management tools, collaborative editors, and data visualization applications benefit from the SPA pattern. Applications where users spend extended sessions performing multiple actions see improved user experience from eliminating page reload latency.
When Traditional Multi-Page Apps Are Better: Content-focused sites (blogs, documentation, marketing pages, e-commerce product listings) where SEO matters critically and interactions are primarily navigation-based often perform better as traditional multi-page applications or using server-side rendering approaches. The initial load time penalty and JavaScript dependency of SPAs can harm user experience for content consumption patterns.
SEO Implications: Search engine crawlers struggle with JavaScript-rendered content. While modern search engines execute JavaScript, coverage remains inconsistent, and crawl budgets may not accommodate slow JavaScript rendering. SPAs requiring strong SEO need server-side rendering (SSR) or pre-rendering strategies to generate HTML for crawlers.
Initial Load Performance: SPAs front-load all application code in the initial bundle, creating larger initial download sizes than traditional multi-page apps that deliver only the code needed for the current page. This impacts time-to-interactive, particularly on slow networks or low-powered devices. Code splitting and lazy loading mitigate this by deferring non-critical code, but the fundamental trade-off remains between initial load time and subsequent navigation speed.
Development Complexity: SPAs introduce complexity in state management, routing, build tooling, and testing. The application must handle concerns traditionally managed by the server: routing, authentication state, data caching, and error handling. Teams must maintain expertise in both backend and frontend technologies, increasing the skills required for full-stack development.
Progressive Enhancement vs Graceful Degradation: Traditional web development emphasizes progressive enhancement—starting with functional HTML and enhancing with JavaScript. SPAs typically follow graceful degradation—assuming JavaScript and degrading to error messages when unavailable. This philosophical difference affects accessibility for users with JavaScript disabled or when JavaScript fails to load.
Deployment and Caching: SPAs require coordination between frontend and backend deployments. API versioning becomes critical to avoid breaking older SPA versions still cached in browsers. HTML5 Application Cache and service workers provide client-side caching but add complexity in cache invalidation and version management.
Browser History and Deep Linking: SPAs must manage browser history explicitly to maintain expected navigation behavior. The History API enables updating URLs without page loads, but implementations must handle edge cases: hash-based routing fallbacks for older browsers, handling external links to deep routes, and coordinating URL state with application state.
# API versioning strategy for managing SPA compatibility
module Api
module V1
class BaseController < ApplicationController
before_action :check_api_version
private
def check_api_version
client_version = request.headers['X-Client-Version']
if client_version && Gem::Version.new(client_version) < Gem::Version.new('2.0.0')
# Handle deprecated clients
response.headers['X-Api-Deprecated'] = 'true'
response.headers['X-Min-Client-Version'] = '2.0.0'
end
end
end
end
module V2
class BaseController < ApplicationController
# New API version with breaking changes
end
end
end
Performance Considerations
Bundle Size Management: The JavaScript bundle size directly impacts time-to-interactive. Modern SPAs can easily exceed multiple megabytes of compressed JavaScript, causing seconds of parse and compile time on average devices. Bundle analysis tools identify large dependencies, and techniques like tree shaking, code splitting, and dynamic imports reduce bundle sizes.
Code Splitting Strategies: Loading all application code upfront wastes bandwidth and processing for features users may never access. Route-based code splitting loads code for specific pages only when navigated to. Component-based splitting defers heavy components until needed. Vendor bundle splitting separates application code from framework code, improving caching when only application code changes.
Initial Load Optimization: Critical rendering path optimization prioritizes resources needed for initial render. Inlining critical CSS, preloading key resources, and deferring non-critical scripts improve perceived performance. The application shell architecture loads a minimal functional shell quickly, then progressively enhances with full functionality.
# Rails helper for generating preload hints
module ApplicationHelper
def preload_links
preload_tags = []
# Preload critical assets
preload_tags << tag.link(
rel: 'preload',
href: asset_path('application.js'),
as: 'script'
)
preload_tags << tag.link(
rel: 'preload',
href: asset_path('application.css'),
as: 'style'
)
# DNS prefetch for API domain
preload_tags << tag.link(
rel: 'dns-prefetch',
href: ENV['API_DOMAIN']
)
safe_join(preload_tags, "\n")
end
end
API Request Optimization: SPAs make numerous API requests, and each request incurs network latency. Request batching combines multiple requests into one. GraphQL enables clients to request exactly the data needed in a single request, avoiding over-fetching and under-fetching issues common with REST APIs. Backend response caching and client-side caching with stale-while-revalidate patterns reduce repeated requests.
Memory Leaks: Long-lived SPAs accumulate memory if event listeners, timers, or references aren't properly cleaned up. Each route transition must dispose of components, clear timers, and remove event listeners. Memory profiling identifies leaks, which commonly occur in third-party libraries, forgotten subscriptions, or circular references preventing garbage collection.
Virtual Scrolling: Lists with thousands of items cause performance issues when rendering all DOM nodes. Virtual scrolling renders only visible items plus a buffer, recycling DOM nodes as the user scrolls. This maintains constant memory usage and smooth scrolling regardless of list size.
Optimistic UI Updates: Network latency makes synchronous API calls feel sluggish. Optimistic updates assume requests succeed, updating the UI immediately and reverting on failure. This makes interactions feel instant but requires careful rollback logic and error handling.
# API endpoint optimized for SPA data loading patterns
module Api
module V1
class DashboardController < ApplicationController
# Single endpoint aggregating dashboard data
def show
data = Rails.cache.fetch("dashboard:#{current_user.id}", expires_in: 5.minutes) do
{
summary: {
total_posts: current_user.posts.count,
total_comments: current_user.comments.count,
unread_notifications: current_user.notifications.unread.count
},
recent_posts: current_user.posts
.order(created_at: :desc)
.limit(5)
.pluck(:id, :title, :created_at),
recent_activity: current_user.activities
.order(created_at: :desc)
.limit(10)
.pluck(:id, :action, :created_at)
}
end
render json: data
end
end
end
end
Service Worker Caching: Service workers cache assets and API responses, enabling offline functionality and instant subsequent loads. Progressive Web Apps (PWAs) leverage service workers to provide app-like experiences. Cache strategies include cache-first (fastest), network-first (freshest), and stale-while-revalidate (balanced).
Prefetching and Preloading: Predictive prefetching loads likely-needed resources before user navigation. Hovering over links, idle time prefetching, and predictive algorithms based on usage patterns improve perceived performance. Resource hints (dns-prefetch, preconnect, prefetch, preload) optimize resource loading.
Security Implications
Cross-Site Scripting (XSS): SPAs dynamically inject user-generated content into the DOM, creating XSS vulnerabilities when content isn't properly sanitized. Modern frameworks like React escape content by default, but dangerouslySetInnerHTML and similar escape hatches bypass protection. Server-side sanitization provides defense in depth.
# Server-side XSS protection for user content
class Post < ApplicationRecord
before_save :sanitize_content
private
def sanitize_content
self.content = ActionController::Base.helpers.sanitize(
content,
tags: %w[p br strong em ul ol li a],
attributes: %w[href]
)
end
end
Authentication Token Security: SPAs store authentication tokens (JWT, OAuth tokens) in the browser. LocalStorage persists across sessions but vulnerable to XSS attacks since JavaScript can access it. SessionStorage clears on tab close, improving security but harming usability. HttpOnly cookies prevent JavaScript access but require CSRF protection and careful CORS configuration.
Cross-Site Request Forgery (CSRF): Traditional CSRF protection using synchronizer tokens doesn't work for stateless API requests. Token-based authentication with tokens in Authorization headers provides CSRF protection since malicious sites cannot set custom headers cross-origin. Stateful cookie-based authentication requires CSRF tokens for state-changing requests.
# CSRF protection for cookie-based authentication
class ApplicationController < ActionController::API
include ActionController::RequestForgeryProtection
protect_from_forgery with: :exception, unless: -> { request.format.json? && bearer_token_auth? }
private
def bearer_token_auth?
request.headers['Authorization']&.start_with?('Bearer ')
end
end
Content Security Policy (CSP): CSP headers restrict resource loading sources, mitigating XSS attacks. SPAs using inline scripts or eval-based template compilation may conflict with strict CSP. Modern build tools generate CSP-compatible bundles, and nonce-based CSP allows specific inline scripts while blocking others.
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https
# API domain for SPA
policy.connect_src :self, ENV['API_DOMAIN']
end
# Generate nonce for inline scripts
Rails.application.config.content_security_policy_nonce_generator = ->(request) {
SecureRandom.base64(16)
}
API Rate Limiting: SPAs can make numerous rapid API requests, whether maliciously or through buggy client code. Rate limiting prevents abuse and protects backend resources. Per-user rate limits based on authenticated identity provide finer control than IP-based limits.
# API rate limiting middleware
class RateLimitMiddleware
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
if request.path.start_with?('/api/')
user_id = current_user_id(request)
key = "rate_limit:#{user_id}:#{Time.now.to_i / 60}"
count = Rails.cache.increment(key, 1, expires_in: 60)
Rails.cache.write(key, 0, expires_in: 60) if count.nil?
if count && count > 100
return [429, {'Content-Type' => 'application/json'}, [{ error: 'Rate limit exceeded' }.to_json]]
end
end
@app.call(env)
end
private
def current_user_id(request)
# Extract user ID from JWT or session
end
end
Secure Dependencies: SPA frameworks and their dependency trees contain numerous packages, any of which could have vulnerabilities. Dependency scanning tools (npm audit, Snyk) identify known vulnerabilities. Keeping dependencies updated and minimizing dependency count reduces attack surface.
API Input Validation: SPAs cannot be trusted for data validation since malicious users can bypass client-side validation. Server-side validation provides the security boundary, treating all client input as untrusted.
# Strong parameters and validation
class PostsController < ApplicationController
def create
post = current_user.posts.build(post_params)
# Server-side validation regardless of client validation
if post.save
render json: { post: post_json(post) }, status: :created
else
render json: {
errors: post.errors.full_messages
}, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :content, :summary).tap do |p|
# Additional sanitization beyond ActiveRecord validation
p[:title] = p[:title].strip.truncate(200) if p[:title]
p[:content] = ActionController::Base.helpers.sanitize(p[:content]) if p[:content]
end
end
end
Tools & Ecosystem
Frontend Frameworks: React, Vue, and Angular dominate SPA development. React uses a component-based architecture with a virtual DOM and JSX syntax. Vue provides progressive framework features with template-based components. Angular offers a complete framework with TypeScript, dependency injection, and RxJS. Svelte compiles components to vanilla JavaScript, eliminating framework runtime overhead.
Build Tools: Vite provides fast development servers with instant hot module replacement. Webpack remains widely used for production builds with extensive plugin ecosystems. esbuild and swc offer dramatically faster JavaScript compilation. Rollup specializes in library bundling with tree-shaking optimization.
State Management: Redux provides predictable state management through unidirectional data flow and immutable updates. MobX uses reactive programming with observable state. Zustand offers lightweight state management without boilerplate. Context API and React hooks enable local state management without external libraries.
Routing Libraries: React Router dominates React SPAs with declarative routing and nested route support. Vue Router provides Vue-specific routing with navigation guards and route-level code splitting. Angular Router integrates with Angular's dependency injection and lazy loading.
API Integration: Axios provides a promise-based HTTP client with request/response interceptors. Fetch API offers native browser support for HTTP requests. TanStack Query (React Query) manages server state caching, background updates, and request deduplication. Apollo Client specializes in GraphQL with normalized caching.
Ruby API Frameworks: Rails API mode creates JSON APIs without view rendering. Grape provides a DSL for creating REST-like APIs with versioning and validation. Sinatra offers lightweight API development. Hanami provides a complete full-stack framework with a focus on clean architecture.
Development Tools: Browser DevTools extensions for React, Vue, and Redux enable component inspection and state debugging. Webpack Bundle Analyzer visualizes bundle composition. Lighthouse audits performance, accessibility, and SEO. Chrome DevTools Performance panel profiles JavaScript execution and rendering.
# Integrating Rails API with modern SPA build tools
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users
resources :posts do
resources :comments
end
end
end
# Serve SPA index.html for all non-API routes
get '*path', to: 'fallback#index', constraints: ->(req) do
!req.xhr? && req.format.html?
end
root 'fallback#index'
end
# app/controllers/fallback_controller.rb
class FallbackController < ActionController::Base
def index
# Serve the SPA entry point with dynamic configuration
@api_url = ENV['API_URL']
@feature_flags = {
new_dashboard: Rails.configuration.x.feature_flags.new_dashboard,
beta_features: Rails.configuration.x.feature_flags.beta_features
}.to_json
render file: Rails.public_path.join('index.html'), layout: false
end
end
Testing Frameworks: Jest provides JavaScript unit testing with snapshot testing and mocking. Vitest offers Vite-native testing with Jest compatibility. Cypress enables end-to-end testing with real browser automation. Testing Library encourages testing user behavior over implementation details. Playwright supports cross-browser end-to-end testing.
TypeScript Integration: TypeScript adds static typing to JavaScript, catching errors at compile time. Type definitions improve IDE autocomplete and refactoring. TypeScript's strict mode enforces type safety, null checking, and comprehensive type coverage.
Common Pitfalls
Memory Leaks from Subscriptions: Event listeners, intervals, and subscriptions created during component lifecycle must be cleaned up. Forgetting cleanup causes memory to grow unbounded as users navigate the application. WebSocket connections, timer intervals, and DOM event listeners commonly leak.
Stale Closure Issues: Closures in callbacks capture variables from outer scopes. When the callback executes later with stale values, unexpected behavior occurs. React hooks particularly suffer from stale closure issues in effects and event handlers when dependencies aren't correctly specified.
Race Conditions in API Requests: Multiple simultaneous API requests for the same resource can complete out of order, causing stale data to overwrite fresh data. Request cancellation, request deduplication, and checking response timestamps prevent race conditions.
# Backend support for handling concurrent updates
class Post < ApplicationRecord
# Optimistic locking to detect concurrent modifications
def self.update_with_lock(id, attributes, expected_version)
post = find(id)
if post.lock_version != expected_version
raise ActiveRecord::StaleObjectError.new(post, 'update')
end
post.update(attributes)
end
end
# API endpoint with version checking
module Api
module V1
class PostsController < ApplicationController
def update
post = Post.find(params[:id])
# Client sends current version
if params[:lock_version].to_i != post.lock_version
render json: {
error: 'Resource was modified',
current_version: post.lock_version,
current_data: post_json(post)
}, status: :conflict
return
end
if post.update(post_params)
render json: { post: post_json(post) }
else
render json: { errors: post.errors }, status: :unprocessable_entity
end
end
end
end
end
Improper Error Handling: Network failures, API errors, and timeout scenarios require explicit error handling. SPAs often display loading states indefinitely when errors occur, leaving users confused. Implementing retry logic, error boundaries, and fallback UI prevents poor user experiences.
Over-Fetching and Under-Fetching: REST APIs often return too much data (over-fetching) or require multiple requests (under-fetching). This wastes bandwidth and slows applications. GraphQL, custom API endpoints combining related resources, and field filtering address these issues.
Authentication Token Management: Storing tokens in localStorage makes them accessible to XSS attacks. Short token expiration improves security but causes frequent re-authentication. Refresh token patterns maintain security while preserving user sessions, but implementations often have flaws allowing token theft or replay attacks.
Hard-Coded API URLs: Embedding API URLs in client code prevents deployment flexibility. Configuration through environment variables or build-time injection enables the same SPA build to work across development, staging, and production environments.
# Providing runtime configuration to SPA
class FallbackController < ActionController::Base
def index
@config = {
api_url: ENV['API_URL'],
ws_url: ENV['WEBSOCKET_URL'],
environment: Rails.env,
version: ENV['APP_VERSION'],
features: feature_flags_for_user
}.to_json
render file: Rails.public_path.join('index.html'), layout: false
end
private
def feature_flags_for_user
return {} unless current_user
{
advanced_editor: current_user.has_feature?(:advanced_editor),
beta_access: current_user.beta_tester?
}
end
end
# index.html includes configuration
# <script>window.__CONFIG__ = <%= raw @config %>;</script>
Ignoring Browser Compatibility: Modern JavaScript features and APIs aren't universally supported. Polyfills, transpilation, and feature detection ensure compatibility across target browsers. Build tools handle most transpilation, but cutting-edge features may require manual polyfilling.
Poor Loading States: Applications loading data display blank screens or spinners without context. Skeleton screens, progressive loading, and optimistic rendering provide better user feedback during loading.
Deep Link Navigation Issues: Users navigating directly to deep routes (via bookmark or external link) expect the full application context. SPAs must reconstruct state from URL parameters and fetch necessary data before rendering, or display appropriate loading states while initializing.
Reference
SPA Architecture Components
| Component | Description | Responsibility |
|---|---|---|
| Router | Maps URLs to views | Client-side navigation, history management, route guards |
| State Manager | Manages application state | Centralized state, state updates, derived state computation |
| API Client | Communicates with backend | HTTP requests, error handling, request cancellation |
| View Layer | Renders UI | Component rendering, user interaction handling |
| Build Tool | Bundles and optimizes code | Code compilation, minification, code splitting |
Authentication Patterns
| Pattern | Storage | Security | Use Case |
|---|---|---|---|
| JWT in LocalStorage | LocalStorage | Vulnerable to XSS | Simple apps with short-lived tokens |
| JWT in Memory | Memory variable | Lost on refresh | High security requirements |
| Token in HttpOnly Cookie | Cookie | Requires CSRF protection | Session-like behavior |
| Refresh Token Pattern | Combination | Complex but secure | Production applications |
API Response Patterns
| Pattern | Structure | Benefit |
|---|---|---|
| Envelope | data, meta, errors objects | Consistent structure, metadata support |
| JSONAPI | Standardized format | Client library support, relationship handling |
| HAL | Hypermedia links | Discoverability, HATEOAS compliance |
| GraphQL | Query-defined shape | Precise data fetching, single endpoint |
Performance Optimization Checklist
| Technique | Impact | Implementation |
|---|---|---|
| Code splitting | High | Route-based or component-based lazy loading |
| Bundle minification | Medium | Production build configuration |
| Tree shaking | Medium | ES6 modules, sideEffects configuration |
| Asset compression | High | Gzip or Brotli server configuration |
| CDN delivery | High | Static asset hosting on CDN |
| Service worker caching | High | Cache-first or network-first strategies |
| Image optimization | Medium | Responsive images, WebP format, lazy loading |
| Prefetching | Low | dns-prefetch, preconnect, prefetch hints |
Security Headers Configuration
| Header | Value | Purpose |
|---|---|---|
| Content-Security-Policy | default-src 'self' | Restrict resource loading sources |
| X-Content-Type-Options | nosniff | Prevent MIME sniffing |
| X-Frame-Options | DENY | Prevent clickjacking |
| Strict-Transport-Security | max-age=31536000 | Enforce HTTPS |
| X-XSS-Protection | 1; mode=block | Browser XSS filter |
Common HTTP Status Codes
| Code | Meaning | SPA Handling |
|---|---|---|
| 200 | Success | Display data |
| 201 | Created | Redirect to new resource |
| 400 | Bad request | Display validation errors |
| 401 | Unauthorized | Redirect to login, clear auth token |
| 403 | Forbidden | Display permission error |
| 404 | Not found | Display not found page |
| 409 | Conflict | Handle concurrent update conflict |
| 422 | Unprocessable entity | Display validation errors |
| 429 | Too many requests | Implement retry with backoff |
| 500 | Server error | Display error, log to monitoring |
State Management Patterns
| Pattern | Complexity | Use Case |
|---|---|---|
| Component State | Low | Local UI state, forms |
| Context API | Low | Shared state across component tree |
| Redux/Store | High | Complex state, time-travel debugging |
| URL State | Low | Shareable application state |
| Server State Cache | Medium | API data caching, background sync |
Ruby API Response Serialization
| Approach | Performance | Flexibility |
|---|---|---|
| Manual JSON | Fast | Full control |
| ActiveModel Serializers | Medium | Declarative, reusable |
| JSONAPI Serializers | Medium | Standards compliant |
| JBuilder | Slow | Template-based, familiar |
| Fast JSON API | Fast | JSONAPI standard, performant |