forked from swapnil.bendal/TypeScript-Backend-Template
[update] - latest code
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
NODE_ENV=
|
||||
PORT=
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_jwt_secret
|
||||
JWT_ACCESS_EXPIRATION_MINUTES=230
|
||||
JWT_REFRESH_EXPIRATION_DAYS=30
|
||||
@@ -8,7 +9,7 @@ JWT_RESET_PASSWORD_EXPIRATION_MINUTES=10
|
||||
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES=10
|
||||
|
||||
|
||||
// DataBase
|
||||
# DataBase
|
||||
DB_USERNAME=username
|
||||
DB_PASSWORD=password
|
||||
DB_DATABASE_NAME=database_name
|
||||
|
||||
3360
package-lock.json
generated
Normal file
3360
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/node": "^16.11.10",
|
||||
"@types/request-ip": "^0.0.41",
|
||||
"nodemon": "^3.1.7",
|
||||
"ts-node": "10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "4.5.2"
|
||||
@@ -24,7 +25,7 @@
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development nodemon src/index.ts",
|
||||
"start": "ts-node src/index.ts",
|
||||
"build" : "tsc",
|
||||
"build": "tsc",
|
||||
"typeorm": "cross-env NODE_ENV=development typeorm-ts-node-commonjs"
|
||||
}
|
||||
}
|
||||
|
||||
11
src/app.ts
11
src/app.ts
@@ -3,10 +3,11 @@ import express, { Application, NextFunction, Request, Response } from "express";
|
||||
import config from "./config/config";
|
||||
import morgan from "./config/morgan";
|
||||
import path from 'path';
|
||||
import ApiError from './utils/helper/ApiError';
|
||||
import { errorConverter, errorHandler } from './middleware/error';
|
||||
import logger from './config/logger';
|
||||
import productRouter from "./routes/productRoutes";
|
||||
import ApiError from './utils/helper/ApiError';
|
||||
import error from './middleware/error';
|
||||
import routes from './routes';
|
||||
|
||||
class App {
|
||||
private app: Application;
|
||||
@@ -30,7 +31,7 @@ class App {
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
this.app.use('/api', productRouter);
|
||||
this.app.use('/api', routes);
|
||||
// Define our routes
|
||||
this.app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
next(new ApiError(404, "Not found"));
|
||||
@@ -38,8 +39,8 @@ class App {
|
||||
}
|
||||
|
||||
private initializeErrorHandling(): void {
|
||||
this.app.use(errorConverter);
|
||||
this.app.use(errorHandler);
|
||||
this.app.use(error.errorConverter);
|
||||
this.app.use(error.errorHandler);
|
||||
}
|
||||
|
||||
public listen(port: number): ReturnType<typeof this.app.listen> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { IProductInteractor } from '../interfaces/IProductInteractor';
|
||||
import { AsyncHandler } from '../utils/handler/AsyncHandler';
|
||||
import ApiResponse from '../utils/helper/ApiResponse';
|
||||
import { AsyncHandler } from '../utils/handler/async.handler';
|
||||
|
||||
export class ProductController {
|
||||
private interactor: IProductInteractor;
|
||||
@@ -18,12 +18,13 @@ export class ProductController {
|
||||
|
||||
@AsyncHandler()
|
||||
async onGetProducts(req: Request, res: Response, next: NextFunction) {
|
||||
const offset = parseInt(`${req.query.offset}`) || 0;
|
||||
const limit = parseInt(`${req.query.limit}`) || 10;
|
||||
const offset = Number.isInteger(Number(req.query.offset)) ? parseInt(`${req.query.offset}`, 10) : 0;
|
||||
const limit = Number.isInteger(Number(req.query.limit)) ? parseInt(`(${req.query.limit}`, 10) : 10;
|
||||
|
||||
const data = await this.interactor.getProducts(limit, offset);
|
||||
|
||||
if (data.length === 0) {
|
||||
res.status(204).json(new ApiResponse(204, null, 'No products found'));
|
||||
res.status(200).json(new ApiResponse(200, null, 'No products found'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ export class ProductController {
|
||||
|
||||
@AsyncHandler()
|
||||
async onUpdateStock(req: Request, res: Response, next: NextFunction) {
|
||||
const data = await this.interactor.updateStock(parseInt(`${req.params.id}`), parseInt(`${req.body.stock}`));
|
||||
const data = await this.interactor.updateStock(parseInt(`${req.params.id}`, 10), parseInt(`${req.body.stock}`, 10));
|
||||
res.status(200).json(new ApiResponse(200, data, 'Successfully updated'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,73 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import ApiError from '../utils/helper/ApiError';
|
||||
import config from '../config/config';
|
||||
import logger from '../config/logger';
|
||||
|
||||
// Middleware to convert different error types to ApiError
|
||||
export const errorConverter = (
|
||||
err: any, // Use a broad type to handle various error types
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void => {
|
||||
let error = err;
|
||||
class error {
|
||||
static errorConverter(
|
||||
err: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
let error = err;
|
||||
|
||||
if (!(err instanceof ApiError)) {
|
||||
// Handle all other errors
|
||||
const statusCode =
|
||||
err.statusCode
|
||||
? 400
|
||||
: 500;
|
||||
const message = err.message || "Internal Server Error";
|
||||
error = new ApiError(statusCode, message, [err.message], false, err.stack);
|
||||
// Handle Sequelize validation and unique constraint errors
|
||||
const messages = error.errors.map((e: Error) => e.message);
|
||||
error = new ApiError(
|
||||
400,
|
||||
messages.join(', '),
|
||||
messages,
|
||||
true,
|
||||
err.stack
|
||||
);
|
||||
if (!(error instanceof ApiError)) {
|
||||
// Handle other errors
|
||||
const statusCode =
|
||||
error.statusCode
|
||||
? 400
|
||||
: 500;
|
||||
const message = error.message || "Something went wrong";
|
||||
error = new ApiError(statusCode, message, error, false, err.stack);
|
||||
}
|
||||
|
||||
next(error);
|
||||
}
|
||||
|
||||
next(error);
|
||||
};
|
||||
static errorHandler(
|
||||
err: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
let { statusCode, message } = err;
|
||||
|
||||
// Middleware to handle errors and send responses
|
||||
export const errorHandler = (
|
||||
err: ApiError,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void => {
|
||||
// Determine the status code and message
|
||||
let { statusCode, message } = err;
|
||||
// Production environment: Ensure only operational errors are shown
|
||||
if (config.env === 'production' && !err.isOperational) {
|
||||
statusCode = 500;
|
||||
message = "Internal server error";
|
||||
}
|
||||
|
||||
if (config.env === 'production' && !err.isOperational) {
|
||||
// Hide sensitive error details in production
|
||||
statusCode = 500;
|
||||
message = "Internal Server Error" as string;
|
||||
res.locals.errorMessage = err.message;
|
||||
|
||||
// Response structure
|
||||
const response = {
|
||||
code: statusCode,
|
||||
message,
|
||||
...(config.env === 'development' && { stack: err.stack })
|
||||
};
|
||||
|
||||
// Log error in development
|
||||
if (config.env === 'development') {
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
// Ensure the response is sent
|
||||
res.status(statusCode).send(response);
|
||||
|
||||
// Don't call next() unless you want to propagate the error further
|
||||
// If this is the final middleware, remove next()
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the error message to response locals for debugging
|
||||
res.locals.errorMessage = err.message;
|
||||
|
||||
// Construct the response object
|
||||
const response = {
|
||||
code: statusCode,
|
||||
message,
|
||||
...(config.env === 'development' && { stack: err.stack }), // Include stack trace in development
|
||||
};
|
||||
|
||||
// Log the error in development mode
|
||||
if (config.env === 'development') {
|
||||
console.error(err.stack || err.message);
|
||||
}
|
||||
|
||||
// Send the error response
|
||||
res.status(statusCode).json(response);
|
||||
};
|
||||
|
||||
export default error;
|
||||
|
||||
45
src/middleware/validate.ts
Normal file
45
src/middleware/validate.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { pick } from '../utils/handler/pick.handler';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import * as Yup from 'yup';
|
||||
import ApiError from '../utils/helper/ApiError';
|
||||
|
||||
/**
|
||||
* Validation middleware for Express routes.
|
||||
* @param schema - Validation schema object (Yup schema for request parts like params, query, body, etc.).
|
||||
* @returns Middleware function to validate request properties.
|
||||
*/
|
||||
const validate =
|
||||
(schema: Partial<Record<keyof Request, Yup.ObjectSchema<any>>>) =>
|
||||
(req: Request, res: Response, next: NextFunction): void => {
|
||||
// Define valid request keys explicitly
|
||||
const validRequestKeys = ['params', 'query', 'body', 'file', 'files'] as (keyof Request)[];
|
||||
|
||||
// Extract the schemas for valid parts of the request
|
||||
const validSchema = pick(schema, validRequestKeys);
|
||||
|
||||
// Extract the corresponding request properties
|
||||
const object = pick(req, Object.keys(validSchema) as (keyof Request)[]);
|
||||
|
||||
// Validate each part of the request
|
||||
const promises = Object.keys(validSchema).map((key) =>
|
||||
validSchema[key as keyof Request]?.validate(object[key as keyof Request], { abortEarly: false })
|
||||
);
|
||||
|
||||
// Process validation results
|
||||
Promise.all(promises)
|
||||
.then((validatedValues) => {
|
||||
// Assign validated values back to the request object
|
||||
validatedValues.forEach((value, index) => {
|
||||
const key = Object.keys(validSchema)[index];
|
||||
(req as any)[key] = value; // Type assertion since req is mutable
|
||||
});
|
||||
next();
|
||||
})
|
||||
.catch((err: Yup.ValidationError) => {
|
||||
// Collect and format error messages
|
||||
const errorMessage = err.inner.map((detail) => detail.message).join(', ');
|
||||
next(new ApiError(400, errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
export default validate;
|
||||
72
src/routes/index.ts
Normal file
72
src/routes/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import express, { Router } from 'express';
|
||||
import config from '../config/config';
|
||||
|
||||
// Define a reusable type for route definitions
|
||||
interface RouteDefinition {
|
||||
path: string;
|
||||
route: () => Router; // Lazy-loaded route
|
||||
}
|
||||
|
||||
class Routes {
|
||||
private router: Router;
|
||||
private defaultRoutes: RouteDefinition[];
|
||||
private devRoutes: RouteDefinition[];
|
||||
|
||||
constructor() {
|
||||
this.router = express.Router();
|
||||
this.defaultRoutes = this.initializeDefaultRoutes();
|
||||
this.devRoutes = this.initializeDevRoutes();
|
||||
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
private initializeDefaultRoutes(): RouteDefinition[] {
|
||||
return [
|
||||
{
|
||||
path: '/products',
|
||||
route: () => require('./productRoutes').default
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
private initializeDevRoutes(): RouteDefinition[] {
|
||||
return [
|
||||
{
|
||||
path: '/docs',
|
||||
route: () => require('./docs/docs.route').default
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
this.defaultRoutes.forEach(({ path, route }) => {
|
||||
this.registerRoute(path, route);
|
||||
});
|
||||
|
||||
this.setupEnvironmentSpecificRoutes();
|
||||
}
|
||||
|
||||
private setupEnvironmentSpecificRoutes(): void {
|
||||
if (config.env === 'development') {
|
||||
this.devRoutes.forEach(({ path, route }) => {
|
||||
this.registerRoute(path, route);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private registerRoute(path: string, route: () => Router): void {
|
||||
try {
|
||||
this.router.use(path, route());
|
||||
} catch (error) {
|
||||
console.error(`Failed to load route at path: ${path}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public getRouter(): Router {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
// Export the initialized router
|
||||
const routes = new Routes();
|
||||
export default routes.getRouter();
|
||||
@@ -10,8 +10,8 @@ const productController = new ProductController(interactor);
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/products', productController.onCreateProduct.bind(productController));
|
||||
router.get('/products', productController.onGetProducts.bind(productController));
|
||||
router.patch('/products/:id', productController.onUpdateStock.bind(productController));
|
||||
router.post('/', productController.onCreateProduct.bind(productController));
|
||||
router.get('/', productController.onGetProducts.bind(productController));
|
||||
router.patch('/:id', productController.onUpdateStock.bind(productController));
|
||||
|
||||
export default router;
|
||||
17
src/utils/handler/pick.handler.ts
Normal file
17
src/utils/handler/pick.handler.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Create an object composed of the picked object properties.
|
||||
* @function pick
|
||||
*
|
||||
* @param object - The source object to pick properties from.
|
||||
* @param keys - The array of property names to pick from the source object.
|
||||
* @returns New object with only the picked properties.
|
||||
*/
|
||||
export const pick = <T extends object, K extends keyof T>(object: T, keys: K[]): Pick<T, K> => {
|
||||
return keys.reduce((result, key) => {
|
||||
// Check if the object has the specified property
|
||||
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
||||
result[key] = object[key]; // Assign the property to the result object
|
||||
}
|
||||
return result;
|
||||
}, {} as Pick<T, K>); // Type the accumulator as Pick<T, K>
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class ApiError extends Error {
|
||||
constructor(statusCode, message = 'Something went wrong', errors = [], isOperational = true, stack = '') {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.data = null;
|
||||
this.message = message;
|
||||
this.success = false;
|
||||
this.errors = errors;
|
||||
this.isOperational = isOperational;
|
||||
if (stack) {
|
||||
this.stack = stack;
|
||||
}
|
||||
else {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.default = ApiError;
|
||||
//# sourceMappingURL=ApiError.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"ApiError.js","sourceRoot":"","sources":["ApiError.ts"],"names":[],"mappings":";;AAAA,MAAM,QAAkB,SAAQ,KAAK;IASjC,YACI,UAAkB,EAClB,UAAkB,sBAAsB,EACxC,SAAqB,EAAE,EACvB,gBAAyB,IAAI,EAC7B,QAAgB,EAAE;QAElB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QAEnC,IAAI,KAAK,EAAE;YACP,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;SACtB;aAAM;YACH,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;SACnD;IACL,CAAC;CACJ;AACD,kBAAe,QAAQ,CAAC"}
|
||||
@@ -1,12 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class ApiResponse {
|
||||
constructor(statusCode, data, message = 'Success') {
|
||||
this.statusCode = statusCode;
|
||||
this.data = data;
|
||||
this.message = message;
|
||||
this.success = statusCode < 400;
|
||||
}
|
||||
}
|
||||
exports.default = ApiResponse;
|
||||
//# sourceMappingURL=ApiResponse.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"ApiResponse.js","sourceRoot":"","sources":["ApiResponse.ts"],"names":[],"mappings":";;AAAA,MAAM,WAAW;IAMb,YAAY,UAAkB,EAAE,IAAc,EAAE,UAAkB,SAAS;QACvE,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,UAAU,GAAG,GAAG,CAAC;IACpC,CAAC;CACJ;AAED,kBAAe,WAAW,CAAC"}
|
||||
Reference in New Issue
Block a user