A production-ready microservices platform for aggregating and comparing product prices across multiple e-commerce platforms. This system demonstrates modern software architecture patterns including service isolation, API gateway pattern, JWT authentication, and containerized deployment with CI/CD automation.
This system implements a microservices architecture with five services communicating over a secure internal Docker bridge network. External traffic flows through the React frontend and Node.js API gateway only, while backend services (Python collector, MongoDB, Redis) remain isolated on the internal network for security.
┌──────────────────────────────────────────┐
│ External Network │
└─────────────┬────────────────────────────┘
│
┌─────────────┴────────────┐
│ User │
└─────────────┬────────────┘
│
┌─────────────────────┴──────────────────────┐
│ │
┌────▼─────┐ ┌─────▼────┐
│ Frontend │ │ API │
│ React │──────────HTTP/REST───────────▶│ Gateway │
│ :3000 │ │ Node.js │
└──────────┘ │ :5000 │
└─────┬────┘
│
┌───────────────────────────────────┼───────────────┐
│ Internal Docker Network │ │
│ (172.28.0.0/16) │ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Python │ │
│ │ Collector │ │
│ │ FastAPI │ │
│ │ :8000 (INT) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────────────────┴──────┐ │
│ │ │ │
│ ┌────▼─────┐ ┌─────▼────┐ │
│ │ MongoDB │ │ Redis │ │
│ │ :27017 │ │ :6379 │ │
│ │ (INT) │ │ (INT) │ │
│ └──────────┘ └──────────┘ │
│ │
└───────────────────────────────────────────────────┘
Legend: (INT) = Internal Only - Not Exposed to Host Network
For comprehensive architecture documentation, see ARCHITECTURE.md.
| Component | Technology | Version | Purpose |
|---|---|---|---|
| Frontend | React | 18.x | User interface and client-side logic |
| API Gateway | Express.js | 4.x | Authentication, routing, rate limiting |
| Collector Service | FastAPI | 0.108+ | Product data aggregation and web scraping |
| Database | MongoDB | 7.0 | Persistent data storage |
| Cache | Redis | 7.x | Session management and query caching |
| Component | Technology | Version | Purpose |
|---|---|---|---|
| Containerization | Docker | 24.x | Service isolation and deployment |
| Orchestration | Docker Compose | 2.x | Multi-container management |
| CI/CD | Jenkins | Latest | Automated build and deployment pipeline |
| Web Server | Nginx | Alpine | Static asset serving for React |
docker --version
docker compose version
git --version
git clone https://github.com/ARaiiler/price-aggregator-microservices.git
cd price-aggregator-microservices
Copy the environment template and configure secrets:
cd infrastructure
cp .env.example .env
Edit .env file with your configuration. Required variables:
# Database
MONGO_ROOT_USERNAME=admin
MONGO_ROOT_PASSWORD=your_secure_mongodb_password
MONGO_DATABASE=priceaggregator
# Cache
REDIS_PASSWORD=your_secure_redis_password
# Authentication
JWT_SECRET=your_jwt_secret_key
# Service URLs (internal - no changes needed)
PYTHON_SERVICE_URL=http://python-collector:8000
FRONTEND_URL=http://localhost:3000
Generate secure secrets:
# JWT Secret
openssl rand -base64 32
# Passwords
openssl rand -hex 16
From the infrastructure directory:
docker compose up -d --build
This command:
# Check container status
docker compose ps
# View logs
docker compose logs -f
# Test health endpoints
curl http://localhost:5000/health
Expected output:
{
"uptime": 123.45,
"message": "OK",
"timestamp": 1708274400000,
"service": "node-gateway",
"environment": "development"
}
Note: Python Collector (port 8000), MongoDB (port 27017), and Redis (port 6379) are not exposed to the host network for security reasons.
# Stop containers (data persists in volumes)
docker compose down
# Stop and remove volumes (deletes all data)
docker compose down -v
This project enforces strict branch protection on the main branch:
# Update main branch
git checkout main
git pull origin main
# Create feature branch
git checkout -b feature/your-feature-name
Branch naming conventions:
feature/ - New featuresbugfix/ - Bug fixeshotfix/ - Production hotfixesrefactor/ - Code refactoringdocs/ - Documentation updatesMake your changes following these guidelines:
Before writing code:
While coding:
CRITICAL: Always test changes locally before pushing.
# Navigate to infrastructure directory
cd infrastructure
# Rebuild affected services
docker compose up -d --build
# Run health checks
docker compose ps
curl http://localhost:5000/health
# Check logs for errors
docker compose logs -f node-gateway
docker compose logs -f python-collector
# Test your feature manually
# (Use Postman, curl, or browser as appropriate)
For Node.js changes:
cd node-gateway
npm test # Run unit tests
npm run lint # Check code style
For Python changes:
cd python-collector
pip install -r requirements.txt
pytest # Run tests when implemented
# Stage changes
git add .
# Commit with descriptive message
git commit -m "feat: add product filtering endpoint
- Implemented /search/filter endpoint
- Added query parameter validation
- Updated API documentation
- Added unit tests"
Commit message format:
feat: - New featurefix: - Bug fixdocs: - Documentationrefactor: - Code refactoringtest: - Adding testschore: - Maintenance tasks# Push branch to remote
git push origin feature/your-feature-name
On GitHub:
Once PR is opened, Jenkins automatically:
If pipeline fails:
If pipeline passes:
After approval and successful CI:
# Remove stopped containers
docker compose down
# Remove all containers and volumes (fresh start)
docker compose down -v
# Remove dangling images
docker image prune -f
# Remove all unused resources
docker system prune -a --volumes
# All services
docker compose logs -f
# Specific service
docker compose logs -f node-gateway
# Last 100 lines
docker compose logs --tail=100 python-collector
# Rebuild only node-gateway
docker compose up -d --build node-gateway
# Rebuild without cache
docker compose build --no-cache python-collector
docker compose up -d python-collector
The project uses Jenkins for continuous integration and continuous deployment. The pipeline is defined in infrastructure/jenkins/Jenkinsfile and automatically triggers on code changes.
┌─────────────────────────────────────────────────────────────┐
│ Jenkins Pipeline Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Checkout │ Clone repository and get commit │
│ │ information for tagging │
│ │ │
│ 2. Environment Check │ Verify Docker and dependencies │
│ │ are available │
│ │ │
│ 3. Build Services │ Parallel build of all Docker │
│ (Parallel) │ images: │
│ │ ├─ Frontend (React + Nginx) │
│ │ ├─ Node Gateway │
│ │ └─ Python Collector │
│ │ │
│ 4. Compose Build │ Build using docker-compose.yml │
│ │ to verify service integration │
│ │ │
│ 5. Security Scan │ (Placeholder) Vulnerability scan │
│ │ Integration point for Trivy/Snyk │
│ │ │
│ 6. Test │ (Placeholder) Run test suites │
│ │ Integration point for pytest/jest │
│ │ │
│ 7. Push Images │ Push to Docker registry │
│ (main only) │ Only on main branch │
│ │ │
│ 8. Deploy │ Deploy to staging/production │
│ (main only) │ Only on main branch │
│ │ │
└─────────────────────────────────────────────────────────────┘
When a Pull Request is created or updated:
When code is merged to main:
Each successful build produces:
{BUILD_NUMBER}-{COMMIT_HASH} and latestTo run the pipeline manually:
# Local build (without Jenkins)
cd infrastructure
docker compose build --no-cache
# Single service build
docker compose build node-gateway
Key environment variables in Jenkinsfile:
DOCKER_IMAGE_PREFIX: Prefix for Docker image namesCOMPOSE_FILE: Path to docker-compose.ymlCOMPOSE_PROJECT_NAME: Project name for Docker ComposeThe system uses a custom Docker bridge network (internal-network) with subnet 172.28.0.0/16 to isolate services from the host and external networks.
Security Model:
External Network (Internet)
│
│ Port exposures: 3000, 5000 only
│
▼
┌───────────────────────────────────────────┐
│ Host Network (Docker Host) │
│ │
│ Exposed Services: │
│ ├─ Frontend:3000 │
│ └─ Node Gateway:5000 │
│ │
└───────────────────────────────────────────┘
│
│ Bridge
│
▼
┌───────────────────────────────────────────┐
│ Internal Docker Network │
│ (172.28.0.0/16) │
│ │
│ All Services (DNS Resolution): │
│ ├─ frontend:3000 │
│ ├─ node-gateway:5000 │
│ ├─ python-collector:8000 (NOT EXPOSED) │
│ ├─ mongodb:27017 (NOT EXPOSED) │
│ └─ redis:6379 (NOT EXPOSED) │
│ │
└───────────────────────────────────────────┘
Services communicate using container names as hostnames:
Example from Node Gateway:
// Connect to Python Collector
const PYTHON_URL = process.env.PYTHON_SERVICE_URL;
// Value: "http://python-collector:8000"
// Connect to MongoDB
const MONGO_URI = process.env.MONGO_URI;
// Value: "mongodb://admin:password@mongodb:27017/priceaggregator"
// Connect to Redis
const REDIS_URL = process.env.REDIS_URL;
// Value: "redis://:password@redis:6379/0"
Why DNS instead of IP addresses:
Security Benefits:
Access Pattern:
docker exec into containersAll custom services implement Docker health checks:
healthcheck:
test: [health check command]
interval: 30s # Run check every 30 seconds
timeout: 5s # Fail if no response in 5 seconds
retries: 3 # Mark unhealthy after 3 failures
start_period: 10s # Grace period after container start
Purpose:
docker compose psDependency Chain:
Frontend → depends_on → Node Gateway → depends_on → Python Collector
↓ ↓
depends_on depends_on
↓ ↓
MongoDB Redis
(healthy) (healthy)
Persistent data stored in Docker named volumes:
mongodb_data: MongoDB database filesmongodb_config: MongoDB configurationredis_data: Redis RDB snapshots and AOF logsVolume Management:
# List volumes
docker volume ls
# Inspect volume
docker volume inspect infrastructure_mongodb_data
# Backup volume
docker run --rm -v infrastructure_mongodb_data:/data -v $(pwd):/backup \
alpine tar czf /backup/mongodb-backup.tar.gz /data
# Restore volume
docker run --rm -v infrastructure_mongodb_data:/data -v $(pwd):/backup \
alpine tar xzf /backup/mongodb-backup.tar.gz -C /
| Variable | Description | Example | Security Level |
|---|---|---|---|
MONGO_ROOT_USERNAME |
MongoDB admin username | admin |
Low |
MONGO_ROOT_PASSWORD |
MongoDB admin password | h8Kf2@mPq9 |
CRITICAL |
MONGO_DATABASE |
Database name | priceaggregator |
Low |
REDIS_PASSWORD |
Redis authentication password | r9Xm#pL2kQ |
HIGH |
JWT_SECRET |
Secret key for JWT signing | base64encodedkey... |
CRITICAL |
| Variable | Description | Default |
|---|---|---|
ENVIRONMENT |
Application environment | production |
NODE_ENV |
Node.js environment | production |
FRONTEND_PORT |
Frontend exposed port | 3000 |
NODE_GATEWAY_PORT |
Gateway exposed port | 5000 |
PYTHON_SERVICE_URL |
Python collector URL | http://python-collector:8000 |
# Strong JWT secret (256-bit)
openssl rand -base64 32
# Strong password (128-bit)
openssl rand -hex 16
# Alternative: Use UUIDs
uuidgen
NEVER:
.env file to version controlALWAYS:
.env to .gitignore (already done)# Restrict .env file permissions (Linux/macOS)
chmod 600 infrastructure/.env
# Verify .env is ignored
git check-ignore infrastructure/.env
# Should output: infrastructure/.env
Copy infrastructure/.env.example to infrastructure/.env:
# ============================================
# Database Configuration
# ============================================
MONGO_ROOT_USERNAME=admin
MONGO_ROOT_PASSWORD=CHANGE_ME_TO_SECURE_PASSWORD
MONGO_DATABASE=priceaggregator
MONGO_URI=mongodb://admin:CHANGE_ME@mongodb:27017/priceaggregator?authSource=admin
# ============================================
# Redis Configuration
# ============================================
REDIS_PASSWORD=CHANGE_ME_TO_SECURE_PASSWORD
REDIS_URL=redis://:CHANGE_ME@redis:6379/0
# ============================================
# JWT Authentication
# ============================================
JWT_SECRET=CHANGE_ME_TO_SECURE_SECRET_MINIMUM_256_BITS
# ============================================
# Service Configuration
# ============================================
ENVIRONMENT=production
NODE_ENV=production
FRONTEND_PORT=3000
NODE_GATEWAY_PORT=5000
PYTHON_SERVICE_URL=http://python-collector:8000
FRONTEND_URL=http://localhost:3000
Contract Notice — This section is the official REST API contract between the React Frontend (
http://localhost:3000) and the Node.js API Gateway (http://localhost:5000). Both sides must honour the request/response shapes defined here. Any change to an endpoint path, field name, type, or HTTP status code is a breaking change and requires a coordinated update on both sides before merging.
| Environment | URL |
|---|---|
| Local development | http://localhost:5000 |
| Inside Docker network | http://node-gateway:5000 |
The frontend resolves the base URL from the REACT_APP_API_URL environment variable (defaults to http://localhost:5000).
// frontend/src/App.js
const API_URL = process.env.REACT_APP_API_URL || "http://localhost:5000";
Protected endpoints require the JWT obtained from POST /auth/login sent as a Bearer token:
Authorization: Bearer <jwt_token>
| Scenario | Status | Response body |
|---|---|---|
Header missing or not Bearer … |
401 |
{ "error": { "message": "No token provided", "status": 401 } } |
| Token invalid or expired | 403 |
{ "error": { "message": "Invalid or expired token", "status": 403 } } |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/ |
No | Service info / liveness ping |
GET |
/health |
No | Gateway health check |
POST |
/auth/register |
No | Create a new user account |
POST |
/auth/login |
No | Log in and receive a JWT |
GET |
/search?query= |
No* | Search products by keyword |
*
/searchdoes not currently enforce the auth middleware, but the middleware is wired and ready. The frontend should always send the token for forward compatibility.
GET /Returns basic service metadata. No authentication required.
Response 200 OK
{
"service": "Node Gateway",
"version": "1.0.0",
"status": "running"
}
| Field | Type | Description |
|---|---|---|
service |
string |
Always "Node Gateway" |
version |
string |
Semantic version of the gateway |
status |
string |
Always "running" when reachable |
GET /healthLive health status of the Node.js Gateway. Used by Docker Compose health checks and the Jenkins pipeline. No authentication required.
Request
GET /health HTTP/1.1
Host: localhost:5000
Response 200 OK
{
"status": "healthy",
"service": "node-gateway",
"timestamp": "2026-02-20T10:00:00.000Z",
"uptime": 12345.67,
"environment": "development"
}
| Field | Type | Description |
|---|---|---|
status |
string |
Always "healthy" while the process is running |
service |
string |
Always "node-gateway" |
timestamp |
string |
ISO 8601 UTC time of the response |
uptime |
number |
Process uptime in seconds (process.uptime()) |
environment |
string |
Value of NODE_ENV (e.g. "development") |
POST /auth/registerCreate a new user account. A JWT is not issued on registration — call POST /auth/login afterwards.
Request
POST /auth/register HTTP/1.1
Host: localhost:5000
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecureP@ssw0rd"
}
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string |
✓ | Valid e-mail address |
password |
string |
✓ | Non-empty; hashed with bcrypt (cost 10) before storage |
Response 201 Created
{
"message": "User registered successfully",
"user": {
"email": "user@example.com"
}
}
| Field | Type | Description |
|---|---|---|
message |
string |
Human-readable confirmation |
user.email |
string |
Echo of the registered e-mail |
Error responses
| Status | Condition | Body |
|---|---|---|
400 |
email or password missing |
{ "error": { "message": "Email and password are required", "status": 400 } } |
500 |
Unexpected server error | { "error": { "message": "Registration failed", "status": 500 } } |
POST /auth/loginAuthenticate with email and password. Returns a signed JWT valid for 24 hours.
Request
POST /auth/login HTTP/1.1
Host: localhost:5000
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecureP@ssw0rd"
}
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string |
✓ | Must match a registered account |
password |
string |
✓ | Plain-text; compared against stored bcrypt hash |
Response 200 OK
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": "24h"
}
| Field | Type | Description |
|---|---|---|
message |
string |
Human-readable confirmation |
token |
string |
Signed JWT — store in memory or sessionStorage; attach to all subsequent requests |
expiresIn |
string |
Token lifetime. Always "24h" |
Decoded JWT payload
{
"email": "user@example.com",
"userId": "placeholder-id",
"iat": 1708258200,
"exp": 1708344600
}
Frontend usage (App.js)
const response = await axios.post(`${API_URL}/auth/login`, { email, password });
const { token } = response.data;
// Attach to protected requests:
// axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
Error responses
| Status | Condition | Body |
|---|---|---|
400 |
email or password missing |
{ "error": { "message": "Email and password are required", "status": 400 } } |
500 |
Unexpected server error | { "error": { "message": "Login failed", "status": 500 } } |
GET /searchSearch for products by keyword. The gateway forwards the query to the internal Python Collector (POST http://python-collector:8000/internal/search) and returns the aggregated, normalised results.
Request
GET /search?query=laptop HTTP/1.1
Host: localhost:5000
Authorization: Bearer <jwt_token>
| Query parameter | Type | Required | Notes |
|---|---|---|---|
query |
string |
✓ | Non-empty; HTML-escaped by the gateway before forwarding |
Frontend usage (App.js)
const response = await axios.get(`${API_URL}/search`, {
params: { query: searchQuery },
});
const products = response.data.results; // rendered as result cards
Response 200 OK
{
"success": true,
"query": "laptop",
"results": [
{
"name": "Laptop - Source A Edition",
"price": 299.99,
"source": "Source A",
"url": "https://source-a.example.com/products/laptop-123",
"currency": "USD",
"in_stock": true,
"timestamp": "2026-02-20T10:30:00.000Z"
},
{
"name": "Laptop - Source B Edition",
"price": 319.99,
"source": "Source B",
"url": "https://source-b.example.com/products/laptop-456",
"currency": "USD",
"in_stock": false,
"timestamp": "2026-02-20T10:30:01.000Z"
}
],
"cached": false,
"sources_queried": 2,
"timestamp": "2026-02-20T10:30:05.000Z"
}
Top-level fields
| Field | Type | Description |
|---|---|---|
success |
boolean |
true when the collector returned a valid response |
query |
string |
Echo of the query parameter sent by the frontend |
results |
array |
List of product objects (see schema below) |
cached |
boolean |
true if the response came from the Redis cache |
sources_queried |
number |
Number of price sources the collector queried |
timestamp |
string |
ISO 8601 UTC time the gateway assembled the response |
Product object schema (each item in results)
| Field | Type | Description | Frontend renders |
|---|---|---|---|
name |
string |
Product title | <h3>{item.name}</h3> |
price |
number |
Price as a decimal number | <p>${item.price}</p> |
source |
string |
Store / scrape-source name | <p>Source: {item.source}</p> |
url |
string |
Direct link to the product listing | — |
currency |
string |
ISO 4217 code (e.g. "USD") |
— |
in_stock |
boolean |
Stock availability at scrape time | — |
timestamp |
string |
ISO 8601 UTC time the price was scraped | — |
Error responses
| Status | Condition | Body |
|---|---|---|
400 |
query param missing or empty |
{ "errors": [{ "msg": "Invalid value", "param": "query", "location": "query" }] } |
503 |
Python Collector is unreachable (ECONNREFUSED) |
{ "success": false, "message": "Product collector service unavailable", "error": "Service temporarily down" } |
500 |
Any other gateway error | { "success": false, "message": "Failed to fetch products", "error": "<error details>" } |
All non-validation errors from the gateway use this shape:
{
"error": {
"message": "Human-readable description",
"status": 400
}
}
404 — Route not found
{
"error": {
"message": "Route not found",
"status": 404
}
}
Before deploying to production:
# Verify .gitignore is working
git status
# .env should NOT appear in untracked files
# If it does, add to .gitignore immediately
On GitHub, configure main branch protection:
main branch:
Team Responsibility: Maintain passing CI at all times.
If CI fails on main:
# Remove unused containers
docker container prune -f
# Remove unused images
docker image prune -a -f
# Remove unused volumes (WARNING: deletes data)
docker volume prune -f
# Complete cleanup
docker system prune -a --volumes -f
# Check Docker disk usage
docker system df
# Detailed usage per type
docker system df -v
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome. Please read the Team Development Workflow section before contributing.
For major changes, please open an issue first to discuss proposed changes.
For questions and issues:
Built for educational purposes demonstrating microservices architecture, containerization, and CI/CD automation.