From 24223903499460e044f264d56f41ec4e53c55466 Mon Sep 17 00:00:00 2001 From: Disura Randunu Date: Mon, 7 Oct 2024 02:40:21 +0530 Subject: [PATCH 1/2] Refactored Error Handleing. Modified Country API using new error handling --- src/AppError.ts | 18 ++++++++++++++++ src/ErrorHandler.ts | 30 +++++++++++++++++++++++++++ src/app.ts | 7 ++++++- src/controllers/country.controller.ts | 27 ++++++++++-------------- src/routes/country/country.route.ts | 9 ++++++-- src/services/country.service.ts | 18 +++++++++++++--- src/utils.ts | 15 +++++++++++++- 7 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 src/AppError.ts create mode 100644 src/ErrorHandler.ts diff --git a/src/AppError.ts b/src/AppError.ts new file mode 100644 index 0000000..ce8170a --- /dev/null +++ b/src/AppError.ts @@ -0,0 +1,18 @@ +export class AppError extends Error { + public readonly name: string + public readonly httpCode: number + public readonly isOperational: boolean + + constructor( + message = 'Internal Server Error', + httpCode = 500, + isOperational = true + ) { + super(message) + Object.setPrototypeOf(this, new.target.prototype) + this.name = this.constructor.name + this.httpCode = httpCode + this.isOperational = isOperational + Error.captureStackTrace(this) + } +} diff --git a/src/ErrorHandler.ts b/src/ErrorHandler.ts new file mode 100644 index 0000000..ae38422 --- /dev/null +++ b/src/ErrorHandler.ts @@ -0,0 +1,30 @@ +import { type Response } from 'express' +import { AppError } from './AppError' + +class ErrorHandler { + public async handleError(error: Error, response: Response): Promise { + console.error(error) + await this.sendErrorResponse(error, response) + } + + private async sendErrorResponse( + error: Error, + response: Response + ): Promise { + if (error instanceof AppError) { + response.status(error.httpCode).json({ + status: false, + message: error.message, + data: null + }) + } else { + response.status(500).json({ + status: false, + message: 'Something went wrong. Please contact administrator.', + data: null + }) + } + } +} + +export const errorHandler = new ErrorHandler() diff --git a/src/app.ts b/src/app.ts index 1fa5cbc..1a1ec0c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,7 @@ import bodyParser from 'body-parser' import cookieParser from 'cookie-parser' import cors from 'cors' -import type { Express } from 'express' +import type { Express, NextFunction, Request, Response } from 'express' import express from 'express' import fs from 'fs' import passport from 'passport' @@ -18,6 +18,7 @@ import mentorRouter from './routes/mentor/mentor.route' import profileRouter from './routes/profile/profile.route' import path from 'path' import countryRouter from './routes/country/country.route' +import { errorHandler } from './ErrorHandler' const app = express() const staticFolder = 'uploads' @@ -48,6 +49,10 @@ app.use('/api/categories', categoryRouter) app.use('/api/emails', emailRouter) app.use('/api/countries', countryRouter) +app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => { + await errorHandler.handleError(err, res) +}) + if (!fs.existsSync(staticFolder)) { fs.mkdirSync(staticFolder, { recursive: true }) console.log('Directory created successfully!') diff --git a/src/controllers/country.controller.ts b/src/controllers/country.controller.ts index 8b1f770..dbad710 100644 --- a/src/controllers/country.controller.ts +++ b/src/controllers/country.controller.ts @@ -1,25 +1,20 @@ import type { Request, Response } from 'express' import type { ApiResponse } from '../types' import type Country from '../entities/country.entity' -import { getAllCountries } from '../services/country.service' +import { getAllCountries, getCountryById } from '../services/country.service' export const getCountries = async ( req: Request, res: Response ): Promise> => { - try { - const data = await getAllCountries() - if (!data) { - return res.status(404).json({ message: 'Countries Not Found' }) - } - return res.status(200).json({ data, message: 'Countries Found' }) - } catch (err) { - if (err instanceof Error) { - console.error('Error executing query', err) - return res - .status(500) - .json({ error: 'Internal server error', message: err.message }) - } - throw err - } + const data = await getAllCountries() + return res.status(200).json({ data, message: 'Countries Found' }) +} + +export const getCountryWithId = async ( + req: Request, + res: Response +): Promise> => { + const data = await getCountryById(req.params.id) + return res.status(200).json({ data, message: 'Country Details' }) } diff --git a/src/routes/country/country.route.ts b/src/routes/country/country.route.ts index e6efa33..2b1e16d 100644 --- a/src/routes/country/country.route.ts +++ b/src/routes/country/country.route.ts @@ -1,8 +1,13 @@ import express from 'express' -import { getCountries } from '../../controllers/country.controller' +import { + getCountries, + getCountryWithId +} from '../../controllers/country.controller' +import { asyncHandler } from '../../utils' const countryRouter = express.Router() -countryRouter.get('/', getCountries) +countryRouter.get('/', asyncHandler(getCountries)) +countryRouter.get('/:id', asyncHandler(getCountryWithId)) export default countryRouter diff --git a/src/services/country.service.ts b/src/services/country.service.ts index 8b754ae..27a82a3 100644 --- a/src/services/country.service.ts +++ b/src/services/country.service.ts @@ -1,13 +1,25 @@ +import { AppError } from '../AppError' import { dataSource } from '../configs/dbConfig' import Country from '../entities/country.entity' -export const getAllCountries = async (): Promise => { +export const getAllCountries = async (): Promise => { const countryRepository = dataSource.getRepository(Country) - const countries: Country[] = await countryRepository.find({ + const countries = await countryRepository.find({ select: ['uuid', 'code', 'name'] }) if (countries && countries.length > 0) { return countries } - return null + throw new AppError('Countries Not Found', 404) +} + +export const getCountryById = async (id: string): Promise => { + const countryRepository = dataSource.getRepository(Country) + const country = await countryRepository.findOneBy({ + uuid: id + }) + if (country) { + return country + } + throw new AppError('Country Not Found', 404) } diff --git a/src/utils.ts b/src/utils.ts index 51d1a0f..aeba2fe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import { JWT_SECRET, CLIENT_URL } from './configs/envConfig' import jwt from 'jsonwebtoken' -import type { Response } from 'express' +import type { NextFunction, Request, Response } from 'express' import type Mentor from './entities/mentor.entity' import path from 'path' import multer from 'multer' @@ -11,6 +11,7 @@ import { randomUUID } from 'crypto' import { certificatesDir } from './app' import type Mentee from './entities/mentee.entity' import { type ZodError } from 'zod' +import { type ApiResponse } from './types' export const signAndSetCookie = (res: Response, uuid: string): void => { const token = jwt.sign({ userId: uuid }, JWT_SECRET ?? '') @@ -299,3 +300,15 @@ export const formatValidationErrors = ( message: `${issue.path.join('.')} is ${issue.message}` })) } + +export const asyncHandler = + ( + fn: ( + req: Request, + res: Response, + next: NextFunction + ) => Promise> + ) => + (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next) + } From d7e05f6fa78d15b1a779bf21597e582b7b22136e Mon Sep 17 00:00:00 2001 From: Disura Randunu Date: Thu, 10 Oct 2024 19:46:05 +0530 Subject: [PATCH 2/2] Error Types added. Factory method added for AppError --- src/AppError.ts | 61 ++++++++++++++++++++++++++++++--- src/services/country.service.ts | 6 ++-- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/AppError.ts b/src/AppError.ts index ce8170a..878edf7 100644 --- a/src/AppError.ts +++ b/src/AppError.ts @@ -2,17 +2,70 @@ export class AppError extends Error { public readonly name: string public readonly httpCode: number public readonly isOperational: boolean + public readonly details?: Record constructor( - message = 'Internal Server Error', - httpCode = 500, - isOperational = true + name: string = AppErrorTypes.INTERNAL_SERVER_ERROR, + message: string = AppErrorDetails[AppErrorTypes.INTERNAL_SERVER_ERROR] + .message, + httpCode: number = AppErrorDetails[AppErrorTypes.INTERNAL_SERVER_ERROR] + .httpCode, + isOperational = true, + details?: Record ) { super(message) Object.setPrototypeOf(this, new.target.prototype) - this.name = this.constructor.name + this.name = name this.httpCode = httpCode this.isOperational = isOperational + this.details = details Error.captureStackTrace(this) } } + +export function createAppError( + errorType: AppErrorTypes, + details?: Record +): AppError { + const { message, httpCode } = AppErrorDetails[errorType] + return new AppError(errorType, message, httpCode, true, details) +} + +export enum AppErrorTypes { + NOT_FOUND_ERROR = 'NOT_FOUND_ERROR', + MULTIPLE_RECORDS_FOUND_ERROR = 'MULTIPLE_RECORDS_FOUND_ERROR', + VALIDATION_ERROR = 'VALIDATION_ERROR', + AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR', + PERMISSION_DENIED_ERROR = 'PERMISSION_DENIED_ERROR', + INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR' +} + +const AppErrorDetails: Record< + AppErrorTypes, + { message: string; httpCode: number } +> = { + [AppErrorTypes.VALIDATION_ERROR]: { + message: 'Invalid input data', + httpCode: 400 + }, + [AppErrorTypes.AUTHENTICATION_ERROR]: { + message: 'Authentication failed', + httpCode: 401 + }, + [AppErrorTypes.NOT_FOUND_ERROR]: { + message: 'Resource not found', + httpCode: 404 + }, + [AppErrorTypes.INTERNAL_SERVER_ERROR]: { + message: 'An internal server error occurred', + httpCode: 500 + }, + [AppErrorTypes.PERMISSION_DENIED_ERROR]: { + message: 'Insufficient permission for this action', + httpCode: 403 + }, + [AppErrorTypes.MULTIPLE_RECORDS_FOUND_ERROR]: { + message: 'Multple records has been found', + httpCode: 400 + } +} diff --git a/src/services/country.service.ts b/src/services/country.service.ts index 27a82a3..e2bd3fa 100644 --- a/src/services/country.service.ts +++ b/src/services/country.service.ts @@ -1,4 +1,4 @@ -import { AppError } from '../AppError' +import { AppErrorTypes, createAppError } from '../AppError' import { dataSource } from '../configs/dbConfig' import Country from '../entities/country.entity' @@ -10,7 +10,7 @@ export const getAllCountries = async (): Promise => { if (countries && countries.length > 0) { return countries } - throw new AppError('Countries Not Found', 404) + throw createAppError(AppErrorTypes.NOT_FOUND_ERROR) } export const getCountryById = async (id: string): Promise => { @@ -21,5 +21,5 @@ export const getCountryById = async (id: string): Promise => { if (country) { return country } - throw new AppError('Country Not Found', 404) + throw createAppError(AppErrorTypes.NOT_FOUND_ERROR) }