Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ pnpm test

---

linked PR 5

## 📄 License

This project is licensed under the ISC License.
36 changes: 36 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { z } from 'zod';

Check failure on line 1 in src/config/env.ts

View workflow job for this annotation

GitHub Actions / build (20.x, 6.0)

Cannot find module 'zod' or its corresponding type declarations.
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<typeof envSchema>;

try {
env = envSchema.parse(process.env);
} catch (error) {
if (error instanceof z.ZodError) {
logger.error('❌ Invalid environment variables:');
error.issues.forEach((err) => {

Check failure on line 27 in src/config/env.ts

View workflow job for this annotation

GitHub Actions / build (20.x, 6.0)

Parameter 'err' implicitly has an 'any' type.

Check failure on line 27 in src/config/env.ts

View workflow job for this annotation

GitHub Actions / build (20.x, 6.0)

'error' is of type 'unknown'.
logger.error(` - ${err.path.join('.')}: ${err.message}`);
});
} else {
logger.error('❌ Failed to parse environment variables:', error);
}
process.exit(1);
}

export default env;
43 changes: 43 additions & 0 deletions src/errors/AppError.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
86 changes: 75 additions & 11 deletions src/middleware/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -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<string, MongooseError.ValidatorError>;
},
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;
Loading