YeboShops Middleware
Documentation of all Express middleware used in the YeboShops API.
Middleware Stack
Applied in app.ts in this order:
typescript
// 1. Trust proxy (Cloud Run)
app.set('trust proxy', true);
// 2. Security headers
app.use(helmet());
// 3. CORS
app.use(cors({ ... }));
// 4. Rate limiting
app.use(limiter);
// 5. Raw body for Stripe webhook
app.use('/api/billing/webhook', express.raw({ type: 'application/json' }));
// 6. Body parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 7. Cookie parsing
app.use(cookieParser());
// 8. Request logging
app.use(requestLogger);
// 9. Country context detection
app.use(countryContextMiddleware);Security Middleware
Helmet
Adds security HTTP headers:
typescript
app.use(helmet());
// Headers set:
// - X-DNS-Prefetch-Control: off
// - X-Frame-Options: SAMEORIGIN
// - X-Content-Type-Options: nosniff
// - Strict-Transport-Security: max-age=15552000
// - X-XSS-Protection: 0
// - X-Download-Options: noopen
// - Referrer-Policy: no-referrerCORS
Cross-Origin Resource Sharing configuration:
typescript
app.use(cors({
origin: config.corsOrigin === '*'
? true
: config.corsOrigin,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
'Accept',
'Origin',
'X-Guest-ID',
'CF-IPCountry', // Cloudflare country header
'CF-Ray',
'CF-Connecting-IP',
'X-Country-Code',
'X-Country-ID',
'X-API-Key'
],
exposedHeaders: ['Content-Range', 'X-Content-Range']
}));Rate Limiting
typescript
const limiter = rateLimit({
windowMs: config.rateLimitWindowMs, // 15 minutes
max: config.rateLimitMax, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
validate: { trustProxy: false, xForwardedForHeader: false }
});
app.use(limiter);Authentication Middleware
JWT Authentication
typescript
// middleware/auth.ts
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
interface AuthRequest extends Request {
user?: {
id: string;
role: string;
};
}
export const authenticate = async (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
try {
// Get token from Authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
// Verify token
const decoded = jwt.verify(token, config.jwtSecret) as {
id: string;
role: string;
};
// Attach user to request
req.user = {
id: decoded.id,
role: decoded.role
};
next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
};
// Optional auth (doesn't fail if no token)
export const optionalAuth = async (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
try {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
const decoded = jwt.verify(token, config.jwtSecret);
req.user = decoded;
}
} catch {
// Ignore errors, continue without auth
}
next();
};Admin Authorization
typescript
export const requireAdmin = (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
if (req.user?.role !== 'ADMIN') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
};API Key Authentication
For internal/webhook routes:
typescript
export const requireApiKey = (
req: Request,
res: Response,
next: NextFunction
) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== config.internalApiKey) {
return res.status(401).json({ error: 'Invalid API key' });
}
next();
};Country Context Middleware
Detects user's country from Cloudflare headers or request headers:
typescript
// middleware/country-context.middleware.ts
export const countryContextMiddleware = async (
req: Request,
res: Response,
next: NextFunction
) => {
// Priority order for country detection:
// 1. X-Country-ID header (explicit)
// 2. X-Country-Code header
// 3. CF-IPCountry (Cloudflare)
// 4. User's profile country (if authenticated)
let countryCode =
req.headers['x-country-code'] as string ||
req.headers['cf-ipcountry'] as string;
if (countryCode) {
// Lookup country in database
const country = await prisma.country.findFirst({
where: {
code: countryCode.toUpperCase(),
isActive: true
}
});
if (country) {
(req as any).countryContext = {
id: country.id,
code: country.code,
currency: country.currency,
callingCode: country.callingCode
};
}
}
next();
};Usage in routes:
typescript
router.get('/products', async (req, res) => {
const countryContext = (req as any).countryContext;
// Filter products by country
const products = await prisma.product.findMany({
where: {
shop: {
countryId: countryContext?.id
}
}
});
// Format prices with local currency
const formatted = products.map(p => ({
...p,
formattedPrice: `${countryContext?.currency?.symbol}${p.priceAmount}`
}));
res.json(formatted);
});Request Logging
typescript
const requestLogger = (req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
const requestId = Math.random().toString(36).substring(7);
// Log request
logger.info(`[${requestId}] ${req.method} ${req.url}`);
// Log response on finish
res.on('finish', () => {
const duration = Date.now() - start;
const statusLevel = res.statusCode >= 400 ? 'warn' : 'info';
logger[statusLevel](`[${requestId}] ${res.statusCode} - ${duration}ms`);
});
next();
};Error Handling Middleware
typescript
// middleware/error.middleware.ts
export const errorHandler = (
error: Error,
req: Request,
res: Response,
next: NextFunction
) => {
// Log error
logger.error('Error:', {
message: error.message,
stack: error.stack,
path: req.path,
method: req.method
});
// Handle known error types
if (error.name === 'ValidationError') {
return res.status(400).json({
success: false,
error: 'Validation Error',
details: error.message
});
}
if (error.name === 'UnauthorizedError') {
return res.status(401).json({
success: false,
error: 'Unauthorized'
});
}
if (error.name === 'NotFoundError') {
return res.status(404).json({
success: false,
error: 'Not Found'
});
}
// Prisma errors
if (error.name === 'PrismaClientKnownRequestError') {
const prismaError = error as any;
if (prismaError.code === 'P2002') {
return res.status(409).json({
success: false,
error: 'Duplicate entry'
});
}
}
// Default server error
return res.status(500).json({
success: false,
error: 'Internal Server Error'
});
};Validation Middleware
Using express-validator or Zod:
typescript
import { body, validationResult } from 'express-validator';
// Validation rules
export const validateCreateProduct = [
body('title')
.notEmpty()
.withMessage('Title is required')
.isLength({ min: 2, max: 200 })
.withMessage('Title must be 2-200 characters'),
body('price')
.optional()
.isFloat({ min: 0 })
.withMessage('Price must be positive'),
body('shopId')
.notEmpty()
.withMessage('Shop ID is required')
];
// Validation check middleware
export const validate = (req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array()
});
}
next();
};
// Usage in routes
router.post('/products',
authenticate,
validateCreateProduct,
validate,
productController.create
);Guest Session Middleware
For tracking anonymous users:
typescript
export const guestSessionMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
// Check for existing guest ID in cookie or header
let guestId =
req.cookies['X-Guest-ID'] ||
req.headers['x-guest-id'] as string;
if (!guestId) {
// Generate new guest ID
guestId = `guest_${crypto.randomUUID()}`;
// Set cookie
res.cookie('X-Guest-ID', guestId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
});
}
(req as any).guestId = guestId;
next();
};Shop Owner Middleware
Verifies user owns the shop:
typescript
export const requireShopOwner = async (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
const shopId = req.params.shopId || req.body.shopId;
if (!shopId) {
return res.status(400).json({ error: 'Shop ID required' });
}
const shop = await prisma.shop.findUnique({
where: { id: shopId },
select: { ownerId: true }
});
if (!shop) {
return res.status(404).json({ error: 'Shop not found' });
}
if (shop.ownerId !== req.user?.id && req.user?.role !== 'ADMIN') {
return res.status(403).json({ error: 'Not shop owner' });
}
next();
};Middleware Chain Example
typescript
// Full route with all middleware
router.post('/products/:productId/publish',
authenticate, // 1. Verify JWT
requireShopOwner, // 2. Verify ownership
validatePublishProduct, // 3. Validate body
validate, // 4. Check validation errors
productController.publish // 5. Handle request
);
// Protected admin route
router.get('/admin/stats',
authenticate,
requireAdmin,
adminController.getStats
);
// Public route with optional auth
router.get('/feed',
optionalAuth,
guestSessionMiddleware,
countryContextMiddleware,
feedController.getFeed
);