Introduction

Modern web applications are no longer single, monolithic systems. Instead, they are composed of multiple layers that must work together coherently: a backend API, a frontend client, a database, and a deployment pipeline.

Designing these systems requires more than choosing frameworks. It requires establishing clear boundaries between components, defining how data flows through the system, and ensuring that changes can be tested, packaged, and deployed reliably.

This article outlines an approach to building and deploying a full-stack application using a Rails API backend and a React + TypeScript frontend, with an emphasis on testing, containerization, and CI/CD workflows.

 

System Architecture Overview

The system is structured as a layered architecture:

React (TypeScript Frontend)

Axios Service Layer

Rails API (JWT Authentication)

PostgreSQL / PostGIS

  • Each layer has a clearly defined responsibility:
  • Frontend handles presentation and user interaction
  • API manages domain logic and authentication
  • Database persists structured data
  • Infrastructure supports deployment and runtime execution

This separation allows each layer to evolve independently while maintaining a stable interface between them.

Backend Architecture: Rails API

The backend is implemented as a Rails 7 API-only application.

Key characteristics:

  • Stateless authentication using Devise + JWT
  • Domain-driven models representing application entities
  • Service objects to isolate business logic from controllers
  • RESTful endpoints for predictable client interaction
  • This structure keeps controllers thin and ensures that business logic is testable in isolation.

Frontend Architecture: React + TypeScript

The frontend is implemented as a React application using TypeScript.

The architecture emphasizes separation between:

  • UI components
  • state management
  • API communication
  • Axios is used as a service layer to centralize HTTP requests:
  • api.get('/events')

This approach avoids scattering network logic throughout components and improves maintainability.

The Role of Testing (RSpec)

One of the most important steps in a production workflow is validating the backend before packaging and deployment.

RSpec is used to test:

  • models (validations, relationships)
  • services (business logic)
  • request specs (API endpoints)

Example:

bundle exec rspec

Why testing comes first

Tests should run before any container build or deployment step.

This ensures:

  • broken code is never packaged into an image
  • regressions are caught early
  • deployments are based on verified application behavior

This is where your intuition is correct — RSpec is a gatekeeper in the workflow.

Containerization with Docker Compose

Once the application passes tests, it is packaged into containers.

Docker Compose defines the runtime environment:

services:
  api:
  build: .
  db:
  image: postgres

Containerization ensures:

  • consistent environments across development and production
  • reproducible builds
  • simplified deployment

CI/CD Workflow with GitHub Actions

The CI/CD pipeline orchestrates the full lifecycle:

 ci cd pipeline

Step-by-step breakdown

  1. Run Tests
bundle exec rspec

If tests fail → pipeline stops

  1. Build Container Image
docker build -t app-image .
  1. Push to GHCR
docker push ghcr.io/your-repo/app-image
  1. Deploy to VPS

On the server:

docker compose pull
docker compose up -d

Correct Mental Model of the Workflow

 

workflow with rspec 

Key insight

Testing is not part of deployment — it is part of validation before packaging.

Think of it this way:

RSpec answers: Is the application correct?

Docker answers: Can the application run consistently?

CI/CD answers: Can the application be delivered reliably?

Why This Architecture Works

This workflow provides several advantages:

  1. Reliability

Code is tested before it is deployed.

  1. Reproducibility

Containers ensure the same environment everywhere.

  1. Separation of concerns

Each layer (frontend, backend, infrastructure) has a clear role.

  1. Scalability

The system can evolve without tightly coupling components.

Practical Application: Amigos Unite

This architecture is reflected in the Amigos Unite project:

Rails API backend with JWT authentication

React frontend with structured service layer

PostgreSQL with PostGIS

Docker Compose for environment consistency

GitHub Actions for CI/CD and deployment

The system demonstrates how these components integrate into a production-ready stack.

Conclusion

Designing a full-stack application for production requires more than writing code. It requires building a system where architecture, testing, containerization, and deployment all work together.

By structuring applications as API-driven systems, validating behavior through testing, and using containerized deployment pipelines, it is possible to create applications that are not only functional, but reliable and maintainable over time.