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:

Step-by-step breakdown
- Run Tests
bundle exec rspec
If tests fail → pipeline stops
- Build Container Image
docker build -t app-image .
- Push to GHCR
docker push ghcr.io/your-repo/app-image
- Deploy to VPS
On the server:
docker compose pull
docker compose up -d
Correct Mental Model of the Workflow
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:
- Reliability
Code is tested before it is deployed.
- Reproducibility
Containers ensure the same environment everywhere.
- Separation of concerns
Each layer (frontend, backend, infrastructure) has a clear role.
- 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.