Skip to content

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,
      },
    });
  }
}

One chat. Everything done.