A full-stack e-commerce/marketplace application with event-driven microservices architecture, built to demonstrate modern backend patterns, asynchronous communication with Redis Streams, and secure deployment practices.
- Architecture Overview
- Project Structure
- Backend Services
- Redis Streams - Event-Driven Communication
- Frontend
- Infrastructure
- Design Patterns
- Security
- Testing
- How to Run
- Next Implementations
This project implements a microservices architecture where services communicate through Redis Streams for asynchronous, reliable message passing.
┌──────────────┐
│ Frontend │ (React + Bootstrap)
│ (Port 8080) │
└──────┬───────┘
│
│ HTTP/REST
│
┌──────▼────────────────────────────────────────────────────┐
│ Docker Network (crud-app) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │Auth-Service │ │Products- │ │Orders- │ │
│ │(Express) │ │Service │ │Service │ │
│ │Port 3030 │ │(Express) │ │(Fastify+TS) │ │
│ └──────┬──────┘ │Port 3020 │ │Port 3040 │ │
│ │ └───┬──────┬───┘ └───┬──────┬───┘ │
│ │ │ │ │ │ │
│ ┌────▼────┐ ┌────▼──┐ │ ┌────▼──┐ │ │
│ │db_auth │ │db_ │ │ │db_ │ │ │
│ │(PG) │ │products│ │ │orders │ │ │
│ └─────────┘ │(PG) │ │ │(PG) │ │ │
│ └───────┘ │ └───────┘ │ │
│ │ │ │
│ ┌─────────▼─────────────────▼──────┐ │
│ │ Redis Streams │ │
│ │ • orders_stream │ │
│ │ • inventory_stream │ │
│ └──────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
| Component | Technology |
|---|---|
| Frontend | React, React Router, Bootstrap 5, Axios |
| Auth-Service | Express.js, PostgreSQL, JWT, bcrypt, Joi |
| Products-Service | Express.js, PostgreSQL, Redis (Consumer) |
| Orders-Service | Fastify, TypeScript, PostgreSQL, Drizzle ORM, Redis (Publisher) |
| Message Broker | Redis Streams (ioredis) |
| Databases | PostgreSQL 17 (3 separate databases) |
| Containerization | Docker, Docker Compose |
| CI/CD | GitHub Actions, Ansible |
| Tunnel | Cloudflare Tunnel |
mini_crud_app/
├── backend/
│ ├── auth-service/ # Authentication & User Management
│ │ ├── controllers/ # HTTP request handlers
│ │ ├── services/ # Business logic layer
│ │ ├── repositories/ # Database access layer
│ │ ├── routes/ # Route definitions
│ │ ├── config/ # Database & JWT config
│ │ ├── __tests__/ # Unit & Integration tests
│ │ │ ├── unit/
│ │ │ └── integration/
│ │ ├── init-scripts/ # Database initialization
│ │ ├── server.js
│ │ ├── app.js
│ │ └── Dockerfile
│ │
│ ├── products-service/ # Product Catalog & Inventory
│ │ ├── consumer/ # Redis Stream consumer
│ │ │ └── ordersConsumer.js # Processes orders from orders_stream
│ │ ├── redis/ # Redis client configuration
│ │ ├── db/ # PostgreSQL connection pool
│ │ ├── init-scripts/ # Database initialization
│ │ ├── products.js # Main service file
│ │ └── Dockerfile
│ │
│ └── orders-service/ # Order Processing (TypeScript)
│ ├── src/
│ │ ├── routes/ # Fastify route handlers
│ │ ├── services/ # Business logic + Redis publisher
│ │ ├── db/ # Drizzle ORM schema & client
│ │ ├── redis/ # Redis client configuration
│ │ ├── middleware/ # JWT authentication hook
│ │ ├── types/ # TypeScript interfaces
│ │ ├── app.ts # Fastify app configuration
│ │ └── server.ts # Server startup
│ ├── drizzle/ # Drizzle ORM migrations
│ ├── init-scripts/ # Database initialization
│ ├── tsconfig.json
│ └── Dockerfile
│
├── frontend/ # React SPA
│ ├── src/
│ │ ├── components/ # React components
│ │ │ ├── BSNavbar.js
│ │ │ ├── CartPage.js
│ │ │ ├── MyHome.js
│ │ │ ├── ProductPage.js
│ │ │ ├── LoginForm.js
│ │ │ └── SignupForm.js
│ │ ├── context/ # React Context API
│ │ │ ├── AuthContext.js # Authentication state
│ │ │ └── CartContext.js # Shopping cart state
│ │ ├── interceptor/ # Axios interceptors
│ │ ├── App.js
│ │ └── index.js
│ ├── public/
│ ├── conf/ # Nginx configuration
│ ├── Dockerfile
│ └── package.json
│
├── ansible/ # Deployment automation
├── .github/workflows/ # CI/CD pipelines
│ └── deploy.yml
├── docker-compose.yml
├── docker-compose.dev.yml
└── .env.example
Port: 3030 (internal)
Database: db_auth (PostgreSQL)
Framework: Express.js
Architecture: Controller-Service-Repository Pattern
- User registration and authentication
- JWT token generation and validation
- Password hashing with bcrypt
- Input validation with Joi
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) DEFAULT 'USER' NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/signup |
Register new user |
| POST | /api/auth/login |
Login with credentials |
| POST | /api/auth/demo-login |
Demo login (for testing) |
Controllers (authController.js)
- Handle HTTP requests/responses
- Delegate business logic to services
- Return appropriate HTTP status codes
Services (authService.js)
- Business logic (validation, hashing, token generation)
- Independent of HTTP layer (reusable)
Repositories (authRepository.js)
- Database queries (CRUD operations)
- Connection pool management
- ✅ Unit Tests: Test individual functions (services/repositories)
- ✅ Integration Tests: Test full HTTP request flow
- Pattern: AAA (Arrange-Act-Assert)
Port: 3020 (internal)
Database: db_products (PostgreSQL)
Framework: Express.js
Redis: Consumer of orders_stream
- CRUD operations for products
- Inventory management (stock quantities)
- Redis Stream Consumer: Listens to
orders_streamand updates inventory
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price NUMERIC(10, 2) NOT NULL,
category VARCHAR(100),
quantity INTEGER DEFAULT 1,
user_id INTEGER NOT NULL, -- Seller ID (external reference)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/products/public/ |
Public catalog (all products) | ❌ |
| GET | /api/products/ |
User's products (seller view) | ✅ |
| GET | /api/products/my-home/ |
Products from other sellers | ✅ |
| GET | /api/products/:id |
Get single product | ✅ |
| POST | /api/products |
Create new product | ✅ |
| PATCH | /api/products/:id |
Update product | ✅ |
| DELETE | /api/products/:id |
Delete product | ✅ |
File: consumer/ordersConsumer.js
What it does:
- Listens to
orders_stream(blocking read withXREAD) - For each order, decrements product quantities in a PostgreSQL transaction
- Publishes result to
inventory_stream:COMPLETEDif all products had sufficient stockFAILEDif any product was out of stock
Code Flow:
// Infinite loop listening to orders_stream
while (true) {
const results = await redis.xread("BLOCK", 0, "STREAMS", "orders_stream", lastId);
for (const msg of messages) {
await processMessage(msg); // Update inventory
}
lastId = messages[messages.length - 1][0]; // Update cursor
}Transaction Example:
await client.query('BEGIN');
try {
for (let item of orderProducts) {
const result = await client.query(
`UPDATE products
SET quantity = quantity - $1
WHERE id = $2 AND quantity >= $1
RETURNING *`,
[quantity, productId]
);
if (result.rowCount === 0) {
orderFailed = true;
break;
}
}
if (!orderFailed) {
await client.query('COMMIT');
await redis.xadd("inventory_stream", "*",
'order_id', orderID,
'status', "COMPLETED"
);
} else {
await client.query('ROLLBACK');
await redis.xadd("inventory_stream", "*",
'order_id', orderID,
'status', "FAILED"
);
}
} catch (error) {
await client.query('ROLLBACK');
}Port: 3040 (internal)
Database: db_orders (PostgreSQL)
Framework: Fastify + TypeScript
ORM: Drizzle ORM
Redis: Publisher to orders_stream
- Create customer orders
- Store order details in database
- Redis Stream Publisher: Publishes new orders to
orders_stream
File: src/db/schema.ts
// orders table
export const orders = pgTable("orders", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
userId: integer("user_id").notNull(), // External reference (auth-service)
totalPrice: decimal("total_price", { precision: 10, scale: 2 }).notNull(),
status: varchar("status", { length: 50 }).default("PENDING"),
createdAt: timestamp("created_at").defaultNow(),
});
// order_items table
export const orderItems = pgTable("order_items", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
orderId: integer("order_id").notNull()
.references(() => orders.id, { onDelete: "cascade" }), // Internal FK
productId: integer("product_id").notNull(), // External reference (products-service)
quantity: integer("quantity").notNull(),
price: decimal("price", { precision: 10, scale: 2 }).notNull(),
});| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/orders |
Create new order | ✅ |
| GET | /health |
Health check | ❌ |
File: src/services/orderService.ts
What it does:
- Receives order data from frontend
- Saves order to database in a Drizzle transaction with status
PENDING - Publishes order details to
orders_streamfor inventory processing
Code Flow:
export async function createOrder(
userId: number,
totalPrice: string,
status: "PENDING" | "COMPLETED" | undefined,
items: IOrderItem[]
): Promise<{ orderId: number; itemCount: number }> {
// 1. Save to database
const orderId = await db.transaction(async (tx) => {
const [orderIDReturned] = await tx
.insert(orders)
.values({ userId, status, totalPrice })
.returning({ insertedId: orders.id });
await tx.insert(orderItems).values(
items.map(item => ({
orderId: orderIDReturned!.insertedId,
productId: item.product_id,
quantity: item.quantity,
price: item.price
}))
);
return orderIDReturned!.insertedId;
});
// 2. Publish to Redis Stream
const redis = await getRedisClient();
await redis.xadd(
"orders_stream",
"*", // auto-generate message ID
'order_id', orderId.toString(),
'products', JSON.stringify(items)
);
return { orderId, itemCount: items.length };
}Redis Streams is a data structure in Redis that acts as a distributed message log (similar to Kafka, but simpler). It's ideal for:
- Asynchronous communication between microservices
- Event-driven architecture
- Persistent message queues
┌──────────────────┐ ┌─────────────┐ ┌──────────────────┐
│ Orders-Service │--------->│ Redis │--------->│ Products-Service │
│ (Producer) │ XADD │ Streams │ XREAD │ (Consumer) │
└──────────────────┘ └─────────────┘ └──────────────────┘
│
│
┌──────────────────┴──────────────────┐
│ │
v v
┌──────────────────┐ ┌──────────────────┐
│ orders_stream │ │ inventory_stream │
└──────────────────┘ └──────────────────┘
Published by: Published by:
orders-service products-service
Consumed by: Consumed by:
products-service orders-service (TODO)
// CartPage.js
const orderData = {
total_price: "1599.98",
status: "PENDING",
items: [
{ product_id: 1, price: "999.99", quantity: 1 },
{ product_id: 2, price: "599.99", quantity: 1 }
]
};
await axios.post('/api/orders', orderData, { withCredentials: true });- Validates the request (Fastify schema validation)
- Extracts user_id from JWT (authMiddleware)
- Saves order to
db_orderswith statusPENDING - Publishes to
orders_stream:
await redis.xadd(
"orders_stream",
"*",
'order_id', '42',
'products', '[{"product_id":1,"price":"999.99","quantity":1}]'
);The consumer (always running) receives the message:
// Reads from orders_stream
const results = await redis.xread("BLOCK", 0, "STREAMS", "orders_stream", lastId);
// Format: [['orders_stream', [['1767783162611-0', ['order_id', '42', 'products', '[...]']]]]]
// Parses message
const orderData = {
order_id: '42',
products: [{ product_id: 1, price: "999.99", quantity: 1 }]
};await client.query('BEGIN');
for (let item of orderProducts) {
const result = await client.query(
`UPDATE products
SET quantity = quantity - $1
WHERE id = $2 AND quantity >= $1
RETURNING *`,
[item.quantity, item.product_id]
);
if (result.rowCount === 0) {
// Product not found or insufficient stock
orderFailed = true;
break;
}
}
if (!orderFailed) {
await client.query('COMMIT');
console.log("✅ Inventory updated successfully");
} else {
await client.query('ROLLBACK');
console.log("❌ Inventory update failed");
}if (!orderFailed) {
await redis.xadd("inventory_stream", "*",
'order_id', '42',
'status', 'COMPLETED'
);
} else {
await redis.xadd("inventory_stream", "*",
'order_id', '42',
'status', 'FAILED'
);
}A consumer in orders-service should read from inventory_stream and update the order status in db_orders.
| Benefit | Description |
|---|---|
| ✅ Decoupling | Services don't call each other directly (loose coupling) |
| ✅ Reliability | Messages are persisted; no data loss if a service crashes |
| ✅ Scalability | Multiple consumers can read from the same stream |
| ✅ Event Sourcing | Complete audit trail of all events |
| ✅ Automatic Retry | Consumers can resume from last processed message |
| ✅ Asynchronous | Non-blocking operations between services |
# Enter Redis container
docker exec -it redis redis-cli
# View all messages in orders_stream from the beginning
XREAD STREAMS orders_stream 0
# View all messages with range
XRANGE orders_stream - +
# View last 5 messages
XREVRANGE orders_stream + - COUNT 5
# View stream info (length, first/last message, etc.)
XINFO STREAM orders_streamFramework: React 18
UI Library: Bootstrap 5
Routing: React Router v6
State Management: Context API
- Authentication: Login/Signup forms with JWT
- Product Catalog: Browse products from all sellers
- My Products: Manage your own products (seller view)
- Shopping Cart: Add/remove items, update quantities
- Checkout: Place orders (sends to orders-service)
- Demo Mode: Test without registration
- Manages user authentication state
- Stores JWT token in HTTP-only cookies
- Provides
isAuth,login,logoutfunctions
- Manages shopping cart state (localStorage persistence)
- Provides
addToCart,removeFromCart,updateQuantity,clearCart,getTotal
| Component | Description |
|---|---|
MyHome.js |
Homepage with product catalog |
ProductPage.js |
Manage user's own products |
CartPage.js |
Shopping cart + checkout |
LoginForm.js |
Login interface |
SignupForm.js |
Registration interface |
BSNavbar.js |
Navigation bar with cart icon |
| Service | Image | Port (Host) | Port (Internal) | Purpose |
|---|---|---|---|---|
frontend |
Custom (Nginx) | 8080 | 80 | React SPA |
auth-service |
Custom (Node) | - | 3030 | Authentication |
products-service |
Custom (Node) | - | 3020 | Product catalog |
orders-service |
Custom (Node) | - | 3040 | Order processing |
db_auth |
postgres:17 | - | 5432 | Auth database |
db_products |
postgres:17 | - | 5432 | Products database |
db_orders |
postgres:17 | - | 5432 | Orders database |
redis |
redis:8.4.0 | - | 6379 | Message broker |
cloudflared |
cloudflare/cloudflared | - | - | Tunnel for HTTPS |
All backend services and databases are isolated within the Docker network (crud-app). Only the frontend (Nginx) is exposed on port 8080.
This follows security best practices:
- Services communicate via internal DNS names (e.g.,
http://auth-service:3030) - No direct external access to databases or backend APIs
- Single entry point through Nginx (acts as reverse proxy + static file server)
All services implement health checks for:
- Ordered startup (
depends_onwithcondition: service_healthy) - Auto-restart on failure
- Monitoring container status
Example:
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3030/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 5svolumes:
postgres_auth_data: # Persists user data
postgres_products_data: # Persists product catalog
postgres_orders_data: # Persists order history
redis_data: # Persists Redis streamsUsed in: auth-service
HTTP Request → Controller → Service → Repository → Database
↓ ↓ ↓
(HTTP logic) (Business) (SQL queries)
Benefits:
- Separation of concerns
- Testability (mock each layer independently)
- Reusability (services can be used by multiple controllers)
Used in: Communication between orders-service and products-service
orders-service → orders_stream → products-service
↓
inventory_stream
Benefits:
- Loose coupling (services don't know about each other)
- Scalability (add more consumers easily)
- Fault tolerance (messages persist even if consumers are down)
Used in: Database connections, Redis clients
let redisClient = null;
export async function getRedisClient() {
if (!redisClient) {
redisClient = new Redis({ ... });
}
return redisClient;
}Benefits:
- Single connection instance (efficient resource usage)
- Lazy initialization (connection created only when needed)
Used in: All database writes affecting multiple tables
await client.query('BEGIN');
try {
await client.query('INSERT INTO orders ...');
await client.query('INSERT INTO order_items ...');
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
}Benefits:
- Atomicity: All operations succeed or all fail
- Consistency: Database stays in valid state
- Isolation: Concurrent transactions don't interfere
Used in: JWT authentication
// Express middleware
function JWT_middleware_decode(req, res, next) {
const token = req.cookies.token;
if (!token) return res.status(401).json({ message: "Unauthorized" });
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Attach user to request
next();
}
app.get('/api/products', JWT_middleware_decode, (req, res) => {
// req.user is available here
});// Fastify hook
server.addHook('preHandler', authMiddleware);Used in: orders-service
// Each route is a plugin
export default async function orderRoutes(server: FastifyInstance) {
server.post('/api/orders', async (request, reply) => { ... });
}
// Register plugin
server.register(orderRoutes, { prefix: '/api/orders' });Used in: Frontend state management
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [isAuth, setIsAuth] = useState(false);
return (
<AuthContext.Provider value={{ isAuth, setIsAuth }}>
{children}
</AuthContext.Provider>
);
}
// Usage in components
const { isAuth } = useContext(AuthContext);| Security Feature | Implementation |
|---|---|
| JWT Authentication | HTTP-only cookies (not localStorage) |
| Password Hashing | bcrypt with salt rounds |
| SQL Injection Prevention | Parameterized queries ($1, $2, etc.) |
| CORS | Configured for specific origins only |
| Security Headers | Helmet.js middleware |
| Input Validation | Joi for auth-service, Fastify schemas for orders-service |
| Network Isolation | Backend services not exposed to internet |
| Environment Variables | Sensitive data in .env (gitignored) |
// ❌ VULNERABLE (never do this!)
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ✅ SAFE (parameterized query)
const query = 'SELECT * FROM users WHERE email = $1';
const result = await pool.query(query, [email]);// Backend sets cookie
res.cookie('token', jwtToken, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict' // CSRF protection
});
// Frontend automatically sends cookie with requests
axios.post('/api/orders', data, { withCredentials: true });- Target: Individual functions (services, repositories)
- Mock: Database connections
- Pattern: AAA (Arrange-Act-Assert)
- Runner: Jest
Example:
test("should hash a password", async () => {
// Arrange
const plaintextPassword = "test12345";
// Act
const hash = await hashPassword(plaintextPassword);
// Assert
expect(hash).toBeDefined();
expect(hash).not.toBe(plaintextPassword);
expect(await bcrypt.compare(plaintextPassword, hash)).toBe(true);
});- Target: Full HTTP request flow (controller → service → repository)
- Mock: Database with
jest.unstable_mockModule - Tool: Supertest
Example:
jest.unstable_mockModule("../../repositories/authRepository.js", () => ({
createUser: jest.fn(),
findUser: jest.fn(),
}));
test("POST /api/auth/signup - success", async () => {
findUser.mockResolvedValueOnce({ rowCount: 0 }); // User doesn't exist
createUser.mockResolvedValueOnce({ rowCount: 1 });
const response = await request(app)
.post('/api/auth/signup')
.send({ username: 'test', email: 'test@test.com', password: 'test123' });
expect(response.status).toBe(201);
expect(response.body.message).toBe('User registered successfully');
});- ⏳ Testing to be implemented
cd backend/auth-service
# Run all tests
npm test
# Run only unit tests
npm run test:unit
# Run only integration tests
npm run test:integration
# Run with coverage
npm run test:coverage- Docker and Docker Compose installed
- Node.js 18+ and npm (for local development)
- Git for cloning the repository
git clone https://github.com/your-username/mini_crud_app.git
cd mini_crud_appcp .env.example .envEdit .env and configure:
- Database credentials (keep defaults for Docker)
- JWT secret (change in production!)
- Cloudflare tunnel token (optional, for external access)
docker-compose up --buildThis will:
- Build all service images
- Create databases with init scripts
- Start Redis
- Start backend services
- Build and serve frontend
- Frontend: http://localhost:8080
- Backend Services: Internal only (not exposed)
- Auth-Service:
http://auth-service:3030(from within Docker network) - Products-Service:
http://products-service:3020 - Orders-Service:
http://orders-service:3040 - Redis:
redis:6379
- Auth-Service:
Two demo users are pre-created:
| Username | Password | Role | |
|---|---|---|---|
| demo | demo@demo.com | demotest | Seller |
| demo2 | demo2@demo.com | demotest | Buyer |
For frontend development with hot reload:
cd frontend
npm install
npm start # Runs on http://localhost:3000Set REACT_APP_IS_DEV=true in .env to use development API URLs.
docker-compose down # Stop containers
docker-compose down -v # Stop and remove volumes (deletes data!)- ✅ JWT-based authentication
- ✅ HTTP-only cookies for token storage
- ✅ Redis Streams for event-driven architecture
- ✅ Database per service (microservices pattern)
- ✅ Docker containerization
- ✅ Health checks for all services
- ✅ Network isolation (security)
- 🔄 Inventory Stream Consumer (orders-service): Listen to
inventory_streamand update order status - 🔄 Refactor products-service: Implement Controller-Service-Repository pattern
-
Role-Based Access Control (RBAC)
- Admin role for user management
- Permissions system
-
Rate Limiting
- Prevent API abuse
- Per-user quotas
-
Structured Logging
- Pino or Winston for JSON logs
- Correlation IDs for tracing requests across services
- Integration with Grafana Loki
-
API Documentation
- Swagger/OpenAPI for REST endpoints
- Auto-generated from code
-
End-to-End (E2E) Tests
- Playwright or Cypress
- Full user journey tests
-
Monitoring & Observability
- Prometheus metrics export
- Grafana dashboards
- Alerting rules
-
Message Acknowledgment
- Redis Stream consumer groups
- Prevent duplicate processing
-
Dead Letter Queue
- Handle failed messages
- Retry logic with exponential backoff
-
CQRS Pattern
- Separate read/write models
- Optimize for query performance
-
WebSockets
- Real-time order status updates
- Live inventory changes
Framework.md- Comparison of backend frameworks (Express, Fastify, NestJS, Spring Boot)jest.md- Testing guide with Jestbackend/orders-service/README.md- Orders-service specific documentation.github/workflows/deploy.yml- CI/CD pipeline configuration
This project is licensed under the MIT License. See LICENSE for details.