From 2b8164575ce6f0f27d165c1cc1c5629fcbc7fb1e Mon Sep 17 00:00:00 2001 From: samad13 Date: Mon, 29 Jun 2026 20:52:48 +0100 Subject: [PATCH 1/2] fix --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e12effc..2ba6041 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,8 @@ pnpm test --- +linked PR 5 + ## 📄 License This project is licensed under the ISC License. From 0a881de9bae38f8e4352dd1b073ddc85d21df3b1 Mon Sep 17 00:00:00 2001 From: samad13 Date: Wed, 1 Jul 2026 11:43:05 +0100 Subject: [PATCH 2/2] fix --- src/config/env.ts | 36 ++++++++++++++ src/errors/AppError.ts | 43 +++++++++++++++++ src/middleware/errorHandler.ts | 86 +++++++++++++++++++++++++++++----- 3 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 src/config/env.ts create mode 100644 src/errors/AppError.ts diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..c9e13fe --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import dotenv from 'dotenv'; +import logger from './logger'; + +dotenv.config(); + +const envSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + PORT: z.coerce.number().int().min(1).max(65535).default(3000), + MONGODB_URI: z.string().url(), + JWT_SECRET: z.string().min(16), + JWT_EXPIRES_IN: z.string().default('7d'), + BCRYPT_ROUNDS: z.coerce.number().int().min(8).max(31).default(10), + LOG_LEVEL: z.enum(['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly']).default('info'), + CORS_ORIGIN: z.string().url(), + RATE_LIMIT_WINDOW_MS: z.coerce.number().int().min(1000).default(900000), + RATE_LIMIT_MAX_REQUESTS: z.coerce.number().int().min(1).default(100), +}); + +let env: z.infer; + +try { + env = envSchema.parse(process.env); +} catch (error) { + if (error instanceof z.ZodError) { + logger.error('❌ Invalid environment variables:'); + error.issues.forEach((err) => { + logger.error(` - ${err.path.join('.')}: ${err.message}`); + }); + } else { + logger.error('❌ Failed to parse environment variables:', error); + } + process.exit(1); +} + +export default env; diff --git a/src/errors/AppError.ts b/src/errors/AppError.ts new file mode 100644 index 0000000..1787541 --- /dev/null +++ b/src/errors/AppError.ts @@ -0,0 +1,43 @@ +export class AppError extends Error { + public readonly statusCode: number; + public readonly isOperational: boolean; + + constructor(message: string, statusCode: number) { + super(message); + + this.statusCode = statusCode; + this.isOperational = true; + + Error.captureStackTrace(this, this.constructor); + } +} + +export class NotFoundError extends AppError { + constructor(message = 'Resource not found') { + super(message, 404); + } +} + +export class BadRequestError extends AppError { + constructor(message = 'Bad request') { + super(message, 400); + } +} + +export class UnauthorizedError extends AppError { + constructor(message = 'Unauthorized') { + super(message, 401); + } +} + +export class ForbiddenError extends AppError { + constructor(message = 'Forbidden') { + super(message, 403); + } +} + +export class ConflictError extends AppError { + constructor(message = 'Conflict') { + super(message, 409); + } +} diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index 0667052..b39387e 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -1,22 +1,86 @@ import { Request, Response, NextFunction } from 'express'; +import { Error as MongooseError } from 'mongoose'; import logger from '../config/logger'; +import env from '../config/env'; +import { AppError } from '../errors/AppError'; -interface AppError extends Error { - statusCode?: number; -} +const handleCastErrorDB = (err: MongooseError.CastError): AppError => { + const message = `Invalid ${err.path}: ${err.value}.`; + return new AppError(message, 400); +}; -const errorHandler = (err: AppError, req: Request, res: Response, _next: NextFunction): void => { - const statusCode = err.statusCode || 500; - const message = err.message || 'Internal Server Error'; +const handleDuplicateFieldsDB = (err: { errmsg?: string; code?: number }): AppError => { + const value = err.errmsg?.match(/(["'])(\\?.)*?\1/)?.[0] || ''; + const message = `Duplicate field value: ${value}. Please use another value!`; + return new AppError(message, 400); +}; - logger.error(`${statusCode} - ${message} - ${req.originalUrl} - ${req.method} - ${req.ip}`); +const handleValidationErrorDB = (err: MongooseError.ValidationError): AppError => { + const errors = Object.values(err.errors).map((el) => el.message); + const message = `Invalid input data. ${errors.join('. ')}`; + return new AppError(message, 400); +}; - res.status(statusCode).json({ +const sendErrorDev = (err: AppError, _req: Request, res: Response): void => { + res.status(err.statusCode).json({ status: 'error', - statusCode, - message, - stack: process.env.NODE_ENV === 'development' ? err.stack : {}, + error: err, + message: err.message, + stack: err.stack, }); }; +const sendErrorProd = (err: AppError, _req: Request, res: Response): void => { + if (err.isOperational) { + res.status(err.statusCode).json({ + status: 'error', + message: err.message, + }); + } else { + logger.error('ERROR 💥', err); + + res.status(500).json({ + status: 'error', + message: 'Something went very wrong!', + }); + } +}; + +const errorHandler = ( + err: Error & { + statusCode?: number; + name?: string; + code?: number; + errmsg?: string; + errors?: Record; + }, + req: Request, + res: Response, + _next: NextFunction, +): void => { + let error: AppError; + + if (err.name === 'CastError') { + error = handleCastErrorDB(err as MongooseError.CastError); + } else if (err.code === 11000) { + error = handleDuplicateFieldsDB(err); + } else if (err.name === 'ValidationError') { + error = handleValidationErrorDB(err as MongooseError.ValidationError); + } else if (err instanceof AppError) { + error = err; + } else { + error = new AppError(err.message || 'Internal Server Error', err.statusCode || 500); + } + + logger.error( + `${error.statusCode} - ${error.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`, + ); + + if (env.NODE_ENV === 'development') { + sendErrorDev(error, req, res); + } else { + sendErrorProd(error, req, res); + } +}; + export default errorHandler;