Intro
Data analysis usually involves juggling multiple tools, such as Pandas for stats, R for tests, and Matplotlib for plots.
I wanted something simpler, so I made a single, containerized app where I could upload a CSV with numeric data and get common summary stats with plots.
The Stats Utility App is a lightweight, polyglot toolkit that runs four services:
- React (frontend)
- Node.js (backend)
- Rust (stats engine)
- Python (plot server)
The app runs completely in Docker and the backend orchestrates all cross-service communication.
In this post, I’ll show how it’s structured, how it runs, and what I learned while piecing four languages together.
Tech Stack
• Frontend: React + Vite + Tailwind + shadcn/ui
• Backend: Node.js (Express + TypeScript…
Intro
Data analysis usually involves juggling multiple tools, such as Pandas for stats, R for tests, and Matplotlib for plots.
I wanted something simpler, so I made a single, containerized app where I could upload a CSV with numeric data and get common summary stats with plots.
The Stats Utility App is a lightweight, polyglot toolkit that runs four services:
- React (frontend)
- Node.js (backend)
- Rust (stats engine)
- Python (plot server)
The app runs completely in Docker and the backend orchestrates all cross-service communication.
In this post, I’ll show how it’s structured, how it runs, and what I learned while piecing four languages together.
Tech Stack
• Frontend: React + Vite + Tailwind + shadcn/ui
• Backend: Node.js (Express + TypeScript)
• Rust Microservice: Axum + serde for high-performance numeric kernels
• Python Microservice: FastAPI + Matplotlib for rendering plots
• Orchestration: Docker + Docker Compose
• Validation: Zod + shared JSON schemas
Everything runs locally in containers and no database is required.
Jobs are stored in memory, keeping it simple and fast to rebuild or demo.
Architecture Overview
frontend/ # React + Tailwind + Vite UI
backend/ # Express API gateway
stats_rs/ # Rust microservice for stats
plots_py/ # Python microservice for plots
docker/ # Compose file + build config
Service flow:
React → Node (Express) → Rust (Axum) → Python (FastAPI)
Each service exposes its own /health endpoint. Docker Compose ensures startup order and readiness before serving the frontend.
Backend Flow
The backend acts as an orchestrator.
When you upload a CSV file, it:
- Reads and validates metadata (Zod schema)
- Sends JSON { values: [..] } to the Rust service
- Waits for summary or distribution results
- Forwards the data to the Python plotter
- Serves JSON + images back to the frontend
Example route:
app.post("/analyze/summary", textCsv, async (req, res) => {
const csv = req.body as string;
const out = await fetchJSON(`${RUST_SVC_URL}/api/v1/stats/summary`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ values: csv.split("\n").map(Number) }),
});
res.json(out);
});
Frontend
The UI (React + Vite) lets users drag-and-drop a CSV file and instantly view:
- Common summary stats (mean, median, sd, IQR, etc.)
- Distribution and ECDF plots
- QQ diagnostic plots
It calls /analyze/* and /plot/* endpoints on the backend, showing a live “Analyzing…” state while the microservices process the request.
Example Output ✅ Summary statistics (mean, median, std, min/max) 📈 Histogram + ECDF + QQ plots 🧮 All computed in Rust and rendered with Matplotlib 💡 Runs entirely in Docker, so setup takes minutes
Environment Setup
# build and run all services
make up
# or manually:
docker compose -f docker/docker-compose.yml up --build
Open http://localhost:8085 to access the app.
Services:
| Service | Port | Description |
|---|---|---|
| frontend | 8085 | React UI (served by Nginx) |
| backend | 8080 | Express API |
| stats_rs | 9000 | Rust microservice |
| plots_py | 7000 | Python microservice |
Lessons Learned
- Rust’s type safety and Axum’s ergonomics make it a good match for numeric microservices.
- FastAPI is ideal for plotting and quick JSON endpoints.
- Zod and Pydantic together make schema validation simple across languages.
- Docker and Docker Compose gave me the most issues out of all aspects of this project, but solving them gave me a much better understanding on how to work with added Docker-related complexity.
- Storing data in memory instead of a database is a quick option for calculations that don’t need to be saved.
Repository + License
📂 Full source: https://github.com/swallace100/stats-utility-app ⚖️ License: MIT