Backend Middleware
All middleware is located in /src/middleware/.
Authentication Middleware
File: auth.middleware.ts
authMiddleware
General authentication middleware - works for both users and employers.
typescript
import { Request, Response, NextFunction } from 'express';
import { JWTUtil, IDecodedToken } from '@utils/jwt';
import { ApiResponse } from '@utils/ApiResponse';
export interface AuthRequest extends Request {
user?: IDecodedToken;
}
export const authMiddleware = (
req: AuthRequest,
res: Response,
next: NextFunction
): void => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
ApiResponse.unauthorized(res, 'No token provided');
return;
}
const token = authHeader.substring(7);
const decoded = JWTUtil.verifyAccessToken(token);
if (!decoded) {
ApiResponse.unauthorized(res, 'Invalid or expired token');
return;
}
req.user = decoded;
next();
} catch (error) {
ApiResponse.unauthorized(res, 'Authentication failed');
}
};Attached to request: req.user with shape:
typescript
interface IDecodedToken {
id: string;
type: 'user' | 'employer';
iat: number;
exp: number;
}employerAuth
Requires authenticated employer specifically.
typescript
export const employerAuth = (
req: AuthRequest,
res: Response,
next: NextFunction
): void => {
authMiddleware(req, res, () => {
if (req.user?.type !== 'employer') {
ApiResponse.forbidden(res, 'Employer access required');
return;
}
next();
});
};userAuth
Requires authenticated user (job seeker/worker) specifically.
typescript
export const userAuth = (
req: AuthRequest,
res: Response,
next: NextFunction
): void => {
authMiddleware(req, res, () => {
if (req.user?.type !== 'user') {
ApiResponse.forbidden(res, 'User access required');
return;
}
next();
});
};optionalAuth
Sets req.user if token is valid, but doesn't require authentication. Useful for endpoints that show different content to authenticated users.
typescript
export const optionalAuth = (
req: AuthRequest,
res: Response,
next: NextFunction
): void => {
try {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
const decoded = JWTUtil.verifyAccessToken(token);
if (decoded) {
req.user = decoded;
}
}
next();
} catch {
// Token invalid, but that's okay for optional auth
next();
}
};Validation Middleware
File: validation.middleware.ts
Validates request body against Joi schemas.
typescript
import { Request, Response, NextFunction } from 'express';
import Joi from 'joi';
import { ApiResponse } from '@utils/ApiResponse';
export const validateRequest = (schema: Joi.ObjectSchema) => {
return (req: Request, res: Response, next: NextFunction): void => {
const { error, value } = schema.validate(req.body, {
abortEarly: false, // Show all errors, not just first
stripUnknown: true, // Remove unknown fields
convert: true, // Type coercion (string "1" → number 1)
});
if (error) {
const errorMessage = error.details
.map((detail) => detail.message)
.join(', ');
ApiResponse.badRequest(res, errorMessage);
return;
}
// Replace body with validated/sanitized value
req.body = value;
next();
};
};Usage
typescript
import { validateRequest } from '@middleware/validation.middleware';
import Joi from 'joi';
const createJobSchema = Joi.object({
title: Joi.string().required().trim(),
salary: Joi.number().required().min(0),
// ...
});
router.post(
'/jobs',
employerAuth,
validateRequest(createJobSchema),
JobController.createJob
);Rate Limiting
File: rateLimit.middleware.ts
Uses express-rate-limit for API protection.
typescript
import rateLimit from 'express-rate-limit';
// General API rate limit
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: {
success: false,
message: 'Too many requests, please try again later',
},
standardHeaders: true,
legacyHeaders: false,
});
// Stricter limit for auth endpoints
export const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // 10 attempts per hour
message: {
success: false,
message: 'Too many login attempts, please try again later',
},
});
// SMS verification rate limit
export const smsLimiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutes
max: 3, // 3 SMS per 10 minutes
message: {
success: false,
message: 'Too many verification requests, please wait before trying again',
},
});Error Handler
File: error.middleware.ts
Global error handler for uncaught exceptions.
typescript
import { Request, Response, NextFunction } from 'express';
import { ApiResponse } from '@utils/ApiResponse';
export const errorHandler = (
error: Error,
req: Request,
res: Response,
_next: NextFunction
): void => {
console.error('Unhandled error:', error);
// Prisma errors
if (error.name === 'PrismaClientKnownRequestError') {
const prismaError = error as any;
if (prismaError.code === 'P2002') {
ApiResponse.conflict(res, 'A record with this value already exists');
return;
}
if (prismaError.code === 'P2025') {
ApiResponse.notFound(res, 'Record not found');
return;
}
}
// JWT errors
if (error.name === 'JsonWebTokenError') {
ApiResponse.unauthorized(res, 'Invalid token');
return;
}
if (error.name === 'TokenExpiredError') {
ApiResponse.unauthorized(res, 'Token expired');
return;
}
// Default server error
ApiResponse.serverError(res, error.message || 'Internal server error', error);
};CORS Middleware
Configured in app.ts:
typescript
import cors from 'cors';
app.use(cors({
origin: [
'http://localhost:5173',
'http://localhost:5174',
'https://yebojobs.pages.dev',
'https://yebojobs.com',
'https://admin.yebojobs.com',
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Webhook-Secret'],
}));Middleware Chain Examples
Public Route (No Auth)
typescript
router.get('/jobs', JobController.getJobs);User-Only Route
typescript
router.post(
'/applications',
userAuth,
validateRequest(createApplicationSchema),
ApplicationController.createApplication
);Employer-Only Route
typescript
router.post(
'/jobs',
employerAuth,
validateRequest(createJobSchema),
JobController.createJob
);Either User or Employer
typescript
router.get(
'/applications/:id',
authMiddleware,
ApplicationController.getApplication
);Optional Auth (Different UX for logged in)
typescript
router.get(
'/services/workers/feed',
optionalAuth,
ServicesController.getWorkerFeed
);JWT Utility
File: utils/jwt.ts
typescript
import jwt from 'jsonwebtoken';
const ACCESS_TOKEN_SECRET = process.env.JWT_SECRET || 'secret';
const REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_SECRET || 'refresh-secret';
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
export interface IDecodedToken {
id: string;
type: 'user' | 'employer';
iat: number;
exp: number;
}
export class JWTUtil {
static generateAccessToken(payload: { id: string; type: 'user' | 'employer' }): string {
return jwt.sign(payload, ACCESS_TOKEN_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY });
}
static generateRefreshToken(payload: { id: string; type: 'user' | 'employer' }): string {
return jwt.sign(payload, REFRESH_TOKEN_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });
}
static verifyAccessToken(token: string): IDecodedToken | null {
try {
return jwt.verify(token, ACCESS_TOKEN_SECRET) as IDecodedToken;
} catch {
return null;
}
}
static verifyRefreshToken(token: string): IDecodedToken | null {
try {
return jwt.verify(token, REFRESH_TOKEN_SECRET) as IDecodedToken;
} catch {
return null;
}
}
}API Response Utility
File: utils/ApiResponse.ts
typescript
import { Response } from 'express';
export class ApiResponse {
static success(res: Response, data: any, message = 'Success'): Response {
return res.status(200).json({ success: true, data, message });
}
static created(res: Response, data: any, message = 'Created'): Response {
return res.status(201).json({ success: true, data, message });
}
static badRequest(res: Response, message: string, data?: any): Response {
return res.status(400).json({ success: false, message, data });
}
static unauthorized(res: Response, message = 'Unauthorized'): Response {
return res.status(401).json({ success: false, message });
}
static forbidden(res: Response, message = 'Forbidden'): Response {
return res.status(403).json({ success: false, message });
}
static notFound(res: Response, message = 'Not found'): Response {
return res.status(404).json({ success: false, message });
}
static conflict(res: Response, message: string): Response {
return res.status(409).json({ success: false, message });
}
static gone(res: Response, message: string): Response {
return res.status(410).json({ success: false, message });
}
static tooManyRequests(res: Response, message: string): Response {
return res.status(429).json({ success: false, message });
}
static serverError(res: Response, message: string, error?: any): Response {
console.error('Server error:', error);
return res.status(500).json({ success: false, message });
}
static paginated(
res: Response,
data: any[],
total: number,
page: number,
limit: number,
message = 'Success'
): Response {
return res.status(200).json({
success: true,
data,
message,
metadata: {
total,
page,
limit,
hasNext: page * limit < total,
},
});
}
}