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.