Skip to main content

React SPA on Cloud Run

A project recipe for a React / TypeScript single-page application backed by a REST API, authenticated with Firebase, and hosted on Google Cloud Run. Use this as a blueprint when estimating, scoping, and delivering a typical B2B internal tool or admin panel.

Project Overview

This recipe targets the most common Aliz frontend engagement: a client-side rendered CRUD application behind authentication. Think user management dashboards, back-office tools, or internal admin panels — projects where SEO is irrelevant and rich interactivity matters.

DecisionChoice
RenderingClient-side SPA
HostingGoogle Cloud Run
LanguageTypeScript
UI frameworkReact
Backend integrationREST API
AuthFirebase Authentication
Primary state modelServer state via TanStack Query

Tech Stack

Core

LibraryRole
ReactUI library
TypeScriptType-safe JavaScript
ViteBuild tool and dev server

Styling & UI

LibraryRole
Tailwind CSSUtility-first CSS
shadcn/uiComponent collection (Radix + Tailwind)

State & Data

LibraryRole
TanStack QueryServer state and data fetching
ZustandClient state management

Routing & Forms

LibraryRole
React RouterClient-side routing
React Hook FormForm state management
ZodSchema validation
date-fnsDate utilities
SonnerToast notifications

Build & Testing

LibraryRole
VitestUnit and integration testing
PlaywrightEnd-to-end testing
tip

All core choices align with the Recommended Tech Stack. See individual pages for rationale and alternatives.

Architecture Overview

The application follows a standard SPA architecture. The browser loads the React application from a container running on Cloud Run. All data flows through a REST API, and Firebase handles user authentication.

The user's browser downloads the SPA bundle from Cloud Run (served via nginx). The SPA communicates directly with the REST API for data and with Firebase for authentication. Firebase-issued tokens are forwarded to the API so the backend can verify the caller's identity.

SPA Component Architecture

Inside the SPA, the application is organized around an app shell, a router, and self-contained feature modules. Each feature module owns its pages, hooks, and API calls.

The App Shell wraps everything in the required providers (auth, query client, state). React Router splits traffic between public routes (login) and protected routes that require authentication. Feature modules are self-contained — each one groups its pages, data-fetching hooks, and types together rather than scattering them across top-level folders.

Key Patterns

API Layer. A centralized fetch wrapper handles injecting the Firebase auth token into every outbound request and provides consistent error handling. Feature modules define their own TanStack Query hooks on top of this wrapper, following the query key factory pattern. This means each feature declares a structured set of query keys (for lists, details, filtered views), which makes cache invalidation predictable — when a mutation succeeds, you invalidate by the relevant key prefix and TanStack Query refetches automatically.

Authentication. Firebase Auth is wrapped in an AuthProvider context that sits near the top of the component tree. A custom hook exposes the current user and loading state to any component that needs it. A ProtectedRoute wrapper checks authentication status before rendering child routes — unauthenticated users are redirected to the login page with their intended destination preserved for post-login redirect. The auth token is retrieved asynchronously and attached to API requests via the centralized fetch wrapper.

State Management. TanStack Query owns all server-derived data — API responses, pagination cursors, cache timestamps. Zustand is reserved for client-only state: sidebar open/closed, theme preference, UI flags. The key rule is to never duplicate server state into a Zustand store. If data comes from an API, TanStack Query should be the single source of truth.

caution

Always enforce authorization server-side. Frontend route guards improve UX but are trivially bypassed. The ProtectedRoute pattern prevents UI flicker and accidental navigation — it does not provide security. See Security.

Task Breakdown

EpicDescriptionBallpark (dev-days)
Project Setup & ToolingScaffold React + TypeScript project, configure build tooling, linting, formatting, folder structure, environment config, CI pipeline2–3
Authentication & AuthorizationFirebase Auth integration, login / register pages, auth context and protected route wrappers, token management3–5
App Shell & NavigationMain layout with sidebar / header, routing setup, breadcrumbs, user menu, responsive shell2–4
API Integration LayerCentralized API client with auth headers, TanStack Query configuration, error handling patterns, query key conventions2–3
CRUD Module 1 (primary entity)List page with table / search / filters / pagination, detail page, create / edit forms with validation — establishes reusable patterns4–6
CRUD Modules 2–3 (additional entities)Replicate patterns from Module 1 for 2–3 more domain entities (effort drops due to pattern reuse)3–6
Dashboard & ReportingSummary page with stat cards, 2–3 charts, date/period filters3–5
Polish & UXLoading / error / empty states, toast notifications, responsive tweaks, accessibility basics2–4
TestingUnit tests for critical logic, component tests, E2E tests for core flows (login, CRUD)3–5
Deployment & InfrastructureContainerized build, Cloud Run deployment, nginx SPA config, CI/CD pipeline, custom domain2–3

Ballpark Totals

MetricRange
Total effort25–45 dev-days
Duration (1 developer)5–9 weeks
Duration (2 developers)3–5 weeks
caution

These are baseline estimates for a straightforward CRUD application with standard complexity. Apply complexity multipliers for drag-and-drop, real-time features, complex forms, i18n, or strict accessibility requirements. Always present estimates as ranges.

What's Not Included

  • Backend API development
  • UX/UI design and Figma work
  • Internationalization — add +10–20% if needed
  • WCAG accessibility audit — add +15–30%
  • Complex data visualizations beyond basic charts
  • Infrastructure provisioning (Terraform, networking)
  • Project management overhead (meetings, demos) — typically +20–30% (see Common Pitfalls)

Deployment Overview

The SPA is deployed as a containerized static site on Google Cloud Run. The build uses a multi-stage Docker approach: a Node.js stage installs dependencies and runs the production build, producing a set of static assets. A second stage copies those assets into an nginx image that serves them.

The nginx configuration handles three concerns. First, SPA fallback routing — any path that doesn't match a static file is rewritten to the root index page so client-side routing works correctly. Second, compression — gzip is enabled for text-based assets to reduce transfer size. Third, caching and security headers — hashed asset files get long-lived cache headers, and standard security headers (frame options, content type sniffing protection, referrer policy) are applied to all responses.

Cloud Run serves the container with automatic TLS, scales to zero when idle (no cost for unused capacity), and requires no infrastructure management. The container must listen on the port specified by the PORT environment variable (default 8080). For latency-sensitive applications, setting a minimum instance count of one avoids cold-start delays. Choose a region close to your users for the best performance.

For CI/CD, a GitHub Actions workflow builds the Docker image and deploys it to Cloud Run on every merge to the main branch. The pipeline typically includes a lint/test step before the deploy step.

tip

The deploy-cloudrun GitHub Action handles GCP authentication and deployment in a single workflow step, simplifying the CI/CD pipeline significantly.

Further Reading

Internal docs:

External resources: