749 lines
18 KiB
Markdown
749 lines
18 KiB
Markdown
# Common Backend Patterns
|
|
|
|
Detailed implementation guides for frequently requested application types.
|
|
|
|
## Pattern 1: CRUD Admin Panel
|
|
|
|
### Recognition Signals
|
|
Frontend shows: List view + Create form + Edit form + Delete button
|
|
|
|
### Implementation
|
|
|
|
#### Database Schema
|
|
```sql
|
|
CREATE TABLE items (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
status VARCHAR(50) DEFAULT 'active',
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
deleted_at TIMESTAMP NULL
|
|
);
|
|
|
|
CREATE INDEX idx_items_status ON items(status);
|
|
CREATE INDEX idx_items_deleted ON items(deleted_at);
|
|
```
|
|
|
|
#### REST API Endpoints
|
|
```javascript
|
|
// List with pagination, filtering, sorting
|
|
GET /api/items?page=1&limit=20&status=active&sort=-createdAt
|
|
|
|
// Create
|
|
POST /api/items
|
|
Body: { name, description, status }
|
|
|
|
// Read
|
|
GET /api/items/:id
|
|
|
|
// Update
|
|
PUT /api/items/:id
|
|
Body: { name, description, status }
|
|
|
|
// Delete (soft)
|
|
DELETE /api/items/:id
|
|
```
|
|
|
|
#### Controller Implementation (Express)
|
|
```javascript
|
|
const itemController = {
|
|
// List with filters
|
|
async list(req, res) {
|
|
const { page = 1, limit = 20, status, sort = '-createdAt' } = req.query;
|
|
|
|
const query = { deleted_at: null };
|
|
if (status) query.status = status;
|
|
|
|
const items = await Item.find(query)
|
|
.sort(sort)
|
|
.skip((page - 1) * limit)
|
|
.limit(limit);
|
|
|
|
const total = await Item.countDocuments(query);
|
|
|
|
res.json({
|
|
data: items,
|
|
pagination: {
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
total,
|
|
totalPages: Math.ceil(total / limit)
|
|
}
|
|
});
|
|
},
|
|
|
|
// Create with validation
|
|
async create(req, res) {
|
|
const { name, description, status } = req.body;
|
|
|
|
if (!name) {
|
|
return res.status(400).json({ error: 'Name is required' });
|
|
}
|
|
|
|
const item = await Item.create({ name, description, status });
|
|
res.status(201).json({ data: item });
|
|
},
|
|
|
|
// Update
|
|
async update(req, res) {
|
|
const item = await Item.findByIdAndUpdate(
|
|
req.params.id,
|
|
req.body,
|
|
{ new: true, runValidators: true }
|
|
);
|
|
|
|
if (!item) {
|
|
return res.status(404).json({ error: 'Item not found' });
|
|
}
|
|
|
|
res.json({ data: item });
|
|
},
|
|
|
|
// Soft delete
|
|
async delete(req, res) {
|
|
const item = await Item.findByIdAndUpdate(
|
|
req.params.id,
|
|
{ deleted_at: new Date() },
|
|
{ new: true }
|
|
);
|
|
|
|
if (!item) {
|
|
return res.status(404).json({ error: 'Item not found' });
|
|
}
|
|
|
|
res.json({ message: 'Item deleted successfully' });
|
|
}
|
|
};
|
|
```
|
|
|
|
#### Admin UI Integration
|
|
Generate admin interface with:
|
|
- Searchable data table
|
|
- Create/Edit modal forms
|
|
- Bulk actions (delete, export)
|
|
- Status filters
|
|
- Date range selectors
|
|
|
|
## Pattern 2: User Authentication Flow
|
|
|
|
### Recognition Signals
|
|
Frontend has: Login page + Protected routes + User profile + Password reset
|
|
|
|
### Implementation
|
|
|
|
#### Database Schema
|
|
```sql
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
first_name VARCHAR(100),
|
|
last_name VARCHAR(100),
|
|
role VARCHAR(50) DEFAULT 'user',
|
|
email_verified BOOLEAN DEFAULT FALSE,
|
|
reset_token VARCHAR(255),
|
|
reset_token_expires TIMESTAMP,
|
|
last_login TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_users_email ON users(email);
|
|
CREATE INDEX idx_users_reset_token ON users(reset_token);
|
|
```
|
|
|
|
#### Authentication API
|
|
```javascript
|
|
// Register
|
|
POST /api/auth/register
|
|
Body: { email, password, firstName, lastName }
|
|
Response: { user, accessToken, refreshToken }
|
|
|
|
// Login
|
|
POST /api/auth/login
|
|
Body: { email, password }
|
|
Response: { user, accessToken, refreshToken }
|
|
|
|
// Refresh token
|
|
POST /api/auth/refresh
|
|
Body: { refreshToken }
|
|
Response: { accessToken }
|
|
|
|
// Logout
|
|
POST /api/auth/logout
|
|
Headers: Authorization: Bearer <token>
|
|
|
|
// Forgot password
|
|
POST /api/auth/forgot-password
|
|
Body: { email }
|
|
|
|
// Reset password
|
|
POST /api/auth/reset-password
|
|
Body: { token, newPassword }
|
|
|
|
// Get current user
|
|
GET /api/auth/me
|
|
Headers: Authorization: Bearer <token>
|
|
Response: { user }
|
|
```
|
|
|
|
#### JWT Implementation
|
|
```javascript
|
|
const jwt = require('jsonwebtoken');
|
|
const bcrypt = require('bcrypt');
|
|
|
|
// Register
|
|
async register(req, res) {
|
|
const { email, password, firstName, lastName } = req.body;
|
|
|
|
// Check if user exists
|
|
const existing = await User.findOne({ email });
|
|
if (existing) {
|
|
return res.status(409).json({ error: 'Email already registered' });
|
|
}
|
|
|
|
// Hash password
|
|
const passwordHash = await bcrypt.hash(password, 10);
|
|
|
|
// Create user
|
|
const user = await User.create({
|
|
email,
|
|
password_hash: passwordHash,
|
|
first_name: firstName,
|
|
last_name: lastName
|
|
});
|
|
|
|
// Generate tokens
|
|
const accessToken = jwt.sign(
|
|
{ userId: user.id, email: user.email },
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: '15m' }
|
|
);
|
|
|
|
const refreshToken = jwt.sign(
|
|
{ userId: user.id },
|
|
process.env.JWT_REFRESH_SECRET,
|
|
{ expiresIn: '7d' }
|
|
);
|
|
|
|
res.status(201).json({
|
|
user: { id: user.id, email: user.email, firstName, lastName },
|
|
accessToken,
|
|
refreshToken
|
|
});
|
|
}
|
|
|
|
// Login
|
|
async login(req, res) {
|
|
const { email, password } = req.body;
|
|
|
|
const user = await User.findOne({ email });
|
|
if (!user) {
|
|
return res.status(401).json({ error: 'Invalid credentials' });
|
|
}
|
|
|
|
const valid = await bcrypt.compare(password, user.password_hash);
|
|
if (!valid) {
|
|
return res.status(401).json({ error: 'Invalid credentials' });
|
|
}
|
|
|
|
// Update last login
|
|
await User.updateOne({ _id: user.id }, { last_login: new Date() });
|
|
|
|
// Generate tokens (same as register)
|
|
// ...
|
|
|
|
res.json({ user, accessToken, refreshToken });
|
|
}
|
|
|
|
// Auth middleware
|
|
const authMiddleware = async (req, res, next) => {
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'No token provided' });
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
req.user = await User.findById(decoded.userId);
|
|
next();
|
|
} catch (error) {
|
|
return res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
};
|
|
```
|
|
|
|
#### Password Reset Flow
|
|
```javascript
|
|
const crypto = require('crypto');
|
|
|
|
// Request reset
|
|
async forgotPassword(req, res) {
|
|
const { email } = req.body;
|
|
const user = await User.findOne({ email });
|
|
|
|
if (!user) {
|
|
// Don't reveal if email exists
|
|
return res.json({ message: 'If email exists, reset link sent' });
|
|
}
|
|
|
|
// Generate reset token
|
|
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
const expires = new Date(Date.now() + 3600000); // 1 hour
|
|
|
|
await User.updateOne(
|
|
{ _id: user.id },
|
|
{ reset_token: resetToken, reset_token_expires: expires }
|
|
);
|
|
|
|
// Send email with reset link
|
|
await sendEmail(user.email, 'Password Reset',
|
|
`Reset your password: ${process.env.FRONTEND_URL}/reset?token=${resetToken}`
|
|
);
|
|
|
|
res.json({ message: 'If email exists, reset link sent' });
|
|
}
|
|
|
|
// Reset password
|
|
async resetPassword(req, res) {
|
|
const { token, newPassword } = req.body;
|
|
|
|
const user = await User.findOne({
|
|
reset_token: token,
|
|
reset_token_expires: { $gt: new Date() }
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(400).json({ error: 'Invalid or expired token' });
|
|
}
|
|
|
|
const passwordHash = await bcrypt.hash(newPassword, 10);
|
|
|
|
await User.updateOne(
|
|
{ _id: user.id },
|
|
{
|
|
password_hash: passwordHash,
|
|
reset_token: null,
|
|
reset_token_expires: null
|
|
}
|
|
);
|
|
|
|
res.json({ message: 'Password reset successfully' });
|
|
}
|
|
```
|
|
|
|
## Pattern 3: Dashboard with Analytics
|
|
|
|
### Recognition Signals
|
|
Frontend shows: Charts + Metrics cards + Filters + Date range selector
|
|
|
|
### Implementation
|
|
|
|
#### Aggregation Queries
|
|
```javascript
|
|
// Total users over time
|
|
const getUserGrowth = async (startDate, endDate) => {
|
|
return await User.aggregate([
|
|
{
|
|
$match: {
|
|
created_at: { $gte: startDate, $lte: endDate }
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: { $dateToString: { format: "%Y-%m-%d", date: "$created_at" } },
|
|
count: { $sum: 1 }
|
|
}
|
|
},
|
|
{ $sort: { _id: 1 } }
|
|
]);
|
|
};
|
|
|
|
// Revenue by category
|
|
const getRevenueByCategory = async () => {
|
|
return await Order.aggregate([
|
|
{ $match: { status: 'completed' } },
|
|
{
|
|
$lookup: {
|
|
from: 'products',
|
|
localField: 'product_id',
|
|
foreignField: '_id',
|
|
as: 'product'
|
|
}
|
|
},
|
|
{ $unwind: '$product' },
|
|
{
|
|
$group: {
|
|
_id: '$product.category',
|
|
total: { $sum: '$amount' }
|
|
}
|
|
}
|
|
]);
|
|
};
|
|
```
|
|
|
|
#### Dashboard API
|
|
```javascript
|
|
GET /api/dashboard/stats
|
|
Query: { startDate, endDate, metric }
|
|
|
|
Response: {
|
|
summary: {
|
|
totalUsers: 1250,
|
|
activeUsers: 450,
|
|
revenue: 125000,
|
|
orders: 340
|
|
},
|
|
charts: {
|
|
userGrowth: [...],
|
|
revenueByCategory: [...],
|
|
ordersByStatus: [...]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Caching Strategy
|
|
```javascript
|
|
const redis = require('redis');
|
|
const client = redis.createClient();
|
|
|
|
const getDashboardStats = async (req, res) => {
|
|
const cacheKey = `dashboard:${req.query.startDate}:${req.query.endDate}`;
|
|
|
|
// Check cache
|
|
const cached = await client.get(cacheKey);
|
|
if (cached) {
|
|
return res.json(JSON.parse(cached));
|
|
}
|
|
|
|
// Compute stats
|
|
const stats = {
|
|
summary: await getSummaryStats(),
|
|
charts: await getChartData()
|
|
};
|
|
|
|
// Cache for 5 minutes
|
|
await client.setex(cacheKey, 300, JSON.stringify(stats));
|
|
|
|
res.json(stats);
|
|
};
|
|
```
|
|
|
|
## Pattern 4: E-commerce System
|
|
|
|
### Recognition Signals
|
|
Frontend has: Product catalog + Cart + Checkout + Order history
|
|
|
|
### Implementation
|
|
|
|
#### Database Schema
|
|
```sql
|
|
-- Products
|
|
CREATE TABLE products (
|
|
id UUID PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
price DECIMAL(10,2) NOT NULL,
|
|
stock_quantity INTEGER DEFAULT 0,
|
|
category VARCHAR(100),
|
|
image_url VARCHAR(500),
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Shopping carts
|
|
CREATE TABLE carts (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE cart_items (
|
|
id UUID PRIMARY KEY,
|
|
cart_id UUID REFERENCES carts(id) ON DELETE CASCADE,
|
|
product_id UUID REFERENCES products(id),
|
|
quantity INTEGER NOT NULL,
|
|
price DECIMAL(10,2) NOT NULL
|
|
);
|
|
|
|
-- Orders
|
|
CREATE TABLE orders (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID REFERENCES users(id),
|
|
total_amount DECIMAL(10,2) NOT NULL,
|
|
status VARCHAR(50) DEFAULT 'pending',
|
|
payment_status VARCHAR(50) DEFAULT 'pending',
|
|
shipping_address TEXT,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE order_items (
|
|
id UUID PRIMARY KEY,
|
|
order_id UUID REFERENCES orders(id),
|
|
product_id UUID REFERENCES products(id),
|
|
quantity INTEGER NOT NULL,
|
|
price DECIMAL(10,2) NOT NULL
|
|
);
|
|
```
|
|
|
|
#### Checkout Flow
|
|
```javascript
|
|
// Add to cart
|
|
POST /api/cart/items
|
|
Body: { productId, quantity }
|
|
|
|
// Update cart item
|
|
PUT /api/cart/items/:id
|
|
Body: { quantity }
|
|
|
|
// Remove from cart
|
|
DELETE /api/cart/items/:id
|
|
|
|
// Checkout
|
|
POST /api/checkout
|
|
Body: { shippingAddress, paymentMethod }
|
|
|
|
// Process order
|
|
1. Validate cart items
|
|
2. Check inventory
|
|
3. Process payment
|
|
4. Create order
|
|
5. Update inventory
|
|
6. Clear cart
|
|
7. Send confirmation email
|
|
```
|
|
|
|
#### Transaction Implementation
|
|
```javascript
|
|
const checkout = async (req, res) => {
|
|
const session = await mongoose.startSession();
|
|
session.startTransaction();
|
|
|
|
try {
|
|
// Get cart
|
|
const cart = await Cart.findOne({ user_id: req.user.id })
|
|
.populate('items.product');
|
|
|
|
// Check inventory
|
|
for (const item of cart.items) {
|
|
if (item.product.stock_quantity < item.quantity) {
|
|
throw new Error(`Insufficient stock for ${item.product.name}`);
|
|
}
|
|
}
|
|
|
|
// Calculate total
|
|
const totalAmount = cart.items.reduce(
|
|
(sum, item) => sum + (item.price * item.quantity), 0
|
|
);
|
|
|
|
// Process payment
|
|
const payment = await processPayment({
|
|
amount: totalAmount,
|
|
method: req.body.paymentMethod
|
|
});
|
|
|
|
// Create order
|
|
const order = await Order.create([{
|
|
user_id: req.user.id,
|
|
total_amount: totalAmount,
|
|
status: 'confirmed',
|
|
payment_status: payment.status,
|
|
shipping_address: req.body.shippingAddress
|
|
}], { session });
|
|
|
|
// Create order items and update inventory
|
|
for (const item of cart.items) {
|
|
await OrderItem.create([{
|
|
order_id: order[0].id,
|
|
product_id: item.product.id,
|
|
quantity: item.quantity,
|
|
price: item.price
|
|
}], { session });
|
|
|
|
await Product.updateOne(
|
|
{ _id: item.product.id },
|
|
{ $inc: { stock_quantity: -item.quantity } },
|
|
{ session }
|
|
);
|
|
}
|
|
|
|
// Clear cart
|
|
await Cart.deleteOne({ _id: cart.id }, { session });
|
|
|
|
await session.commitTransaction();
|
|
|
|
// Send confirmation email (outside transaction)
|
|
await sendOrderConfirmation(req.user.email, order[0]);
|
|
|
|
res.json({ order: order[0] });
|
|
|
|
} catch (error) {
|
|
await session.abortTransaction();
|
|
res.status(400).json({ error: error.message });
|
|
} finally {
|
|
session.endSession();
|
|
}
|
|
};
|
|
```
|
|
|
|
## Pattern 5: Social Features
|
|
|
|
### Recognition Signals
|
|
Frontend shows: Posts + Comments + Likes + Follow/Unfollow buttons + Activity feed
|
|
|
|
### Implementation
|
|
|
|
#### Database Schema
|
|
```sql
|
|
-- Posts
|
|
CREATE TABLE posts (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID REFERENCES users(id),
|
|
content TEXT NOT NULL,
|
|
image_url VARCHAR(500),
|
|
likes_count INTEGER DEFAULT 0,
|
|
comments_count INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Comments
|
|
CREATE TABLE comments (
|
|
id UUID PRIMARY KEY,
|
|
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
|
|
user_id UUID REFERENCES users(id),
|
|
content TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Likes
|
|
CREATE TABLE likes (
|
|
user_id UUID REFERENCES users(id),
|
|
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
PRIMARY KEY (user_id, post_id)
|
|
);
|
|
|
|
-- Follows
|
|
CREATE TABLE follows (
|
|
follower_id UUID REFERENCES users(id),
|
|
following_id UUID REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
PRIMARY KEY (follower_id, following_id)
|
|
);
|
|
|
|
CREATE INDEX idx_posts_user ON posts(user_id, created_at DESC);
|
|
CREATE INDEX idx_follows_follower ON follows(follower_id);
|
|
CREATE INDEX idx_follows_following ON follows(following_id);
|
|
```
|
|
|
|
#### Activity Feed Implementation
|
|
```javascript
|
|
// Get personalized feed
|
|
const getFeed = async (req, res) => {
|
|
const userId = req.user.id;
|
|
const { page = 1, limit = 20 } = req.query;
|
|
|
|
// Get posts from followed users + own posts
|
|
const posts = await Post.aggregate([
|
|
{
|
|
$lookup: {
|
|
from: 'follows',
|
|
localField: 'user_id',
|
|
foreignField: 'following_id',
|
|
as: 'follows'
|
|
}
|
|
},
|
|
{
|
|
$match: {
|
|
$or: [
|
|
{ user_id: userId },
|
|
{ 'follows.follower_id': userId }
|
|
]
|
|
}
|
|
},
|
|
{
|
|
$lookup: {
|
|
from: 'users',
|
|
localField: 'user_id',
|
|
foreignField: '_id',
|
|
as: 'author'
|
|
}
|
|
},
|
|
{
|
|
$lookup: {
|
|
from: 'likes',
|
|
let: { postId: '$_id' },
|
|
pipeline: [
|
|
{ $match: { $expr: { $and: [
|
|
{ $eq: ['$post_id', '$$postId'] },
|
|
{ $eq: ['$user_id', userId] }
|
|
]}}}
|
|
],
|
|
as: 'userLike'
|
|
}
|
|
},
|
|
{ $sort: { created_at: -1 } },
|
|
{ $skip: (page - 1) * limit },
|
|
{ $limit: limit },
|
|
{
|
|
$project: {
|
|
content: 1,
|
|
image_url: 1,
|
|
likes_count: 1,
|
|
comments_count: 1,
|
|
created_at: 1,
|
|
author: { $arrayElemAt: ['$author', 0] },
|
|
isLiked: { $gt: [{ $size: '$userLike' }, 0] }
|
|
}
|
|
}
|
|
]);
|
|
|
|
res.json({ posts });
|
|
};
|
|
|
|
// Like/Unlike post
|
|
const toggleLike = async (req, res) => {
|
|
const { postId } = req.params;
|
|
const userId = req.user.id;
|
|
|
|
const existing = await Like.findOne({ user_id: userId, post_id: postId });
|
|
|
|
if (existing) {
|
|
// Unlike
|
|
await Like.deleteOne({ _id: existing.id });
|
|
await Post.updateOne({ _id: postId }, { $inc: { likes_count: -1 } });
|
|
res.json({ liked: false });
|
|
} else {
|
|
// Like
|
|
await Like.create({ user_id: userId, post_id: postId });
|
|
await Post.updateOne({ _id: postId }, { $inc: { likes_count: 1 } });
|
|
|
|
// Create notification
|
|
await Notification.create({
|
|
user_id: post.user_id,
|
|
type: 'like',
|
|
content: `${req.user.name} liked your post`,
|
|
post_id: postId
|
|
});
|
|
|
|
res.json({ liked: true });
|
|
}
|
|
};
|
|
```
|
|
|
|
## Pattern Selection Guide
|
|
|
|
When analyzing frontend/UI, look for these indicators:
|
|
|
|
| Frontend Feature | Suggested Pattern |
|
|
|------------------|-------------------|
|
|
| Data table + forms | CRUD Admin Panel |
|
|
| Login page + profile | User Authentication |
|
|
| Charts + metrics | Dashboard Analytics |
|
|
| Product list + cart | E-commerce |
|
|
| Posts + social interactions | Social Features |
|
|
|
|
Combine multiple patterns as needed for complex applications.
|