create-express-mongoose-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { ApiResponseHandler } from "../utils/response";
3
+ import { ApiError } from "../utils/apiError";
4
+ import { logger } from "../config/winston-logger";
5
+ import { StatusCodes } from "http-status-codes";
6
+
7
+ export const errorMiddleware = (
8
+ err: any,
9
+ req: Request,
10
+ res: Response,
11
+ next: NextFunction,
12
+ ) => {
13
+ const requestId = req.id || "unknown";
14
+
15
+ // Handle Mongoose validation errors
16
+ if (err.name === "ValidationError") {
17
+ const errors: Record<string, any> = {};
18
+ Object.keys(err.errors).forEach((key) => {
19
+ errors[key] = err.errors[key].message;
20
+ });
21
+ logger.error(`[${requestId}] Validation Error:`, errors);
22
+ return ApiResponseHandler.error(
23
+ res,
24
+ "Validation failed",
25
+ StatusCodes.UNPROCESSABLE_ENTITY,
26
+ errors,
27
+ );
28
+ }
29
+
30
+ // Handle Mongoose cast errors
31
+ if (err.name === "CastError") {
32
+ logger.error(`[${requestId}] Invalid ID format`);
33
+ return ApiResponseHandler.error(
34
+ res,
35
+ "Invalid ID format",
36
+ StatusCodes.BAD_REQUEST,
37
+ );
38
+ }
39
+
40
+ // Handle duplicate key errors
41
+ if (err.code === 11000) {
42
+ const field = Object.keys(err.keyPattern)[0];
43
+ logger.error(`[${requestId}] Duplicate key error on field: ${field}`);
44
+ return ApiResponseHandler.error(
45
+ res,
46
+ `${field} already exists`,
47
+ StatusCodes.CONFLICT,
48
+ );
49
+ }
50
+
51
+ // Handle JWT errors
52
+ if (err.name === "JsonWebTokenError") {
53
+ logger.error(`[${requestId}] JWT Error: ${err.message}`);
54
+ return ApiResponseHandler.error(
55
+ res,
56
+ "Invalid token",
57
+ StatusCodes.UNAUTHORIZED,
58
+ );
59
+ }
60
+
61
+ if (err.name === "TokenExpiredError") {
62
+ logger.error(`[${requestId}] Token Expired`);
63
+ return ApiResponseHandler.error(
64
+ res,
65
+ "Token expired",
66
+ StatusCodes.UNAUTHORIZED,
67
+ );
68
+ }
69
+
70
+ // Handle custom ApiError
71
+ if (err instanceof ApiError) {
72
+ logger.error(`[${requestId}] ApiError: ${err.message}`, err.errors);
73
+ return ApiResponseHandler.error(
74
+ res,
75
+ err.message,
76
+ err.statusCode,
77
+ err.errors,
78
+ );
79
+ }
80
+
81
+ // Handle unexpected errors
82
+ logger.error(`[${requestId}] Unexpected Error:`, err.message);
83
+ return ApiResponseHandler.error(
84
+ res,
85
+ "Internal server error",
86
+ StatusCodes.INTERNAL_SERVER_ERROR,
87
+ );
88
+ };
@@ -0,0 +1,20 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { v4 as uuidv4 } from "uuid";
3
+
4
+ declare global {
5
+ namespace Express {
6
+ interface Request {
7
+ id?: string;
8
+ }
9
+ }
10
+ }
11
+
12
+ export const requestIdMiddleware = (
13
+ req: Request,
14
+ res: Response,
15
+ next: NextFunction,
16
+ ) => {
17
+ req.id = uuidv4();
18
+ res.setHeader("X-Request-ID", req?.id!);
19
+ next();
20
+ };
@@ -0,0 +1,45 @@
1
+ import { Request, Response } from "express";
2
+ import { ExampleService } from "./example.service";
3
+ import { ApiResponseHandler } from "../../utils/response";
4
+ import { asyncHandler } from "../../utils/asyncHandler";
5
+
6
+ const exampleService = new ExampleService();
7
+
8
+ export const getAllExamples = asyncHandler(
9
+ async (req: Request, res: Response) => {
10
+ const examples = await exampleService.findAll();
11
+ ApiResponseHandler.success(
12
+ res,
13
+ examples,
14
+ "Examples retrieved successfully",
15
+ );
16
+ },
17
+ );
18
+
19
+ export const getExampleById = asyncHandler(
20
+ async (req: Request, res: Response) => {
21
+ const example = await exampleService.findById(req.params.id);
22
+ ApiResponseHandler.success(res, example, "Example retrieved successfully");
23
+ },
24
+ );
25
+
26
+ export const createExample = asyncHandler(
27
+ async (req: Request, res: Response) => {
28
+ const example = await exampleService.create(req.body);
29
+ ApiResponseHandler.created(res, example, "Example created successfully");
30
+ },
31
+ );
32
+
33
+ export const updateExample = asyncHandler(
34
+ async (req: Request, res: Response) => {
35
+ const example = await exampleService.update(req.params.id, req.body);
36
+ ApiResponseHandler.success(res, example, "Example updated successfully");
37
+ },
38
+ );
39
+
40
+ export const deleteExample = asyncHandler(
41
+ async (req: Request, res: Response) => {
42
+ await exampleService.delete(req.params.id);
43
+ ApiResponseHandler.noContent(res);
44
+ },
45
+ );
@@ -0,0 +1,31 @@
1
+ import mongoose, { Schema, Document } from "mongoose";
2
+
3
+ export interface IExample extends Document {
4
+ name: string;
5
+ description?: string;
6
+ isActive: boolean;
7
+ createdAt: Date;
8
+ updatedAt: Date;
9
+ }
10
+
11
+ const ExampleSchema = new Schema(
12
+ {
13
+ name: {
14
+ type: String,
15
+ required: [true, "Name is required"],
16
+ trim: true,
17
+ unique: true,
18
+ },
19
+ description: {
20
+ type: String,
21
+ trim: true,
22
+ },
23
+ isActive: {
24
+ type: Boolean,
25
+ default: true,
26
+ },
27
+ },
28
+ { timestamps: true },
29
+ );
30
+
31
+ export default mongoose.model<IExample>("Example", ExampleSchema);
@@ -0,0 +1,20 @@
1
+ import { Router } from "express";
2
+ import {
3
+ getAllExamples,
4
+ getExampleById,
5
+ createExample,
6
+ updateExample,
7
+ deleteExample,
8
+ } from "./example.controller";
9
+ import { validateWithJoi } from "../../utils/validationMiddleware";
10
+ import { createExampleSchema, updateExampleSchema } from "./example.validation";
11
+
12
+ const router = Router();
13
+
14
+ router.get("/", getAllExamples);
15
+ router.get("/:id", getExampleById);
16
+ router.post("/", validateWithJoi(createExampleSchema), createExample);
17
+ router.put("/:id", validateWithJoi(updateExampleSchema), updateExample);
18
+ router.delete("/:id", deleteExample);
19
+
20
+ export default router;
@@ -0,0 +1,92 @@
1
+ import { StatusCodes } from "http-status-codes";
2
+ import { ApiError } from "../../utils/apiError";
3
+ import Example, { IExample } from "./example.model";
4
+
5
+ export class ExampleService {
6
+ async findAll() {
7
+ try {
8
+ return await Example.find({ isActive: true })
9
+ .sort({ createdAt: -1 })
10
+ .lean();
11
+ } catch (error: any) {
12
+ throw ApiError.internal("Failed to retrieve examples");
13
+ }
14
+ }
15
+
16
+ async findById(id: string): Promise<IExample> {
17
+ try {
18
+ const example = await Example.findById(id);
19
+ if (!example) {
20
+ throw ApiError.notFound("Example not found");
21
+ }
22
+ return example;
23
+ } catch (error: any) {
24
+ if (error instanceof ApiError) throw error;
25
+ if (error.kind === "ObjectId") {
26
+ throw ApiError.badRequest("Invalid ID format");
27
+ }
28
+ throw ApiError.internal("Failed to retrieve example");
29
+ }
30
+ }
31
+
32
+ async create(data: Partial<IExample>): Promise<IExample> {
33
+ try {
34
+ const example = new Example(data);
35
+ return await example.save();
36
+ } catch (error: any) {
37
+ if (error.name === "ValidationError") {
38
+ const messages = Object.values(error.errors)
39
+ .map((err: any) => err.message)
40
+ .join(", ");
41
+ throw ApiError.unprocessable(messages);
42
+ }
43
+ if (error.code === 11000) {
44
+ const field = Object.keys(error.keyPattern)[0];
45
+ throw ApiError.conflict(`${field} already exists`);
46
+ }
47
+ throw ApiError.internal("Failed to create example");
48
+ }
49
+ }
50
+
51
+ async update(id: string, data: Partial<IExample>): Promise<IExample> {
52
+ try {
53
+ const example = await Example.findByIdAndUpdate(id, data, {
54
+ new: true,
55
+ runValidators: true,
56
+ });
57
+
58
+ if (!example) {
59
+ throw ApiError.notFound("Example not found");
60
+ }
61
+
62
+ return example;
63
+ } catch (error: any) {
64
+ if (error instanceof ApiError) throw error;
65
+ if (error.kind === "ObjectId") {
66
+ throw ApiError.badRequest("Invalid ID format");
67
+ }
68
+ if (error.name === "ValidationError") {
69
+ const messages = Object.values(error.errors)
70
+ .map((err: any) => err.message)
71
+ .join(", ");
72
+ throw ApiError.unprocessable(messages);
73
+ }
74
+ throw ApiError.internal("Failed to update example");
75
+ }
76
+ }
77
+
78
+ async delete(id: string): Promise<void> {
79
+ try {
80
+ const result = await Example.findByIdAndDelete(id);
81
+ if (!result) {
82
+ throw ApiError.notFound("Example not found");
83
+ }
84
+ } catch (error: any) {
85
+ if (error instanceof ApiError) throw error;
86
+ if (error.kind === "ObjectId") {
87
+ throw ApiError.badRequest("Invalid ID format");
88
+ }
89
+ throw ApiError.internal("Failed to delete example");
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,16 @@
1
+ import Joi from "joi";
2
+
3
+ export const createExampleSchema = Joi.object({
4
+ name: Joi.string().required().trim().messages({
5
+ "string.empty": "Name is required",
6
+ "any.required": "Name is required",
7
+ }),
8
+ description: Joi.string().optional().trim(),
9
+ isActive: Joi.boolean().optional().default(true),
10
+ });
11
+
12
+ export const updateExampleSchema = Joi.object({
13
+ name: Joi.string().optional().trim(),
14
+ description: Joi.string().optional().trim(),
15
+ isActive: Joi.boolean().optional(),
16
+ });
@@ -0,0 +1,57 @@
1
+ import http from "http";
2
+ import { createApp } from "./app";
3
+ import { config } from "./config/env";
4
+ import { connectDatabase, disconnectDatabase } from "./config/database";
5
+ import { logger } from "./config/winston-logger";
6
+
7
+ let server: http.Server;
8
+
9
+ export const startServer = async () => {
10
+ try {
11
+ // Connect to database
12
+ await connectDatabase();
13
+
14
+ // Create Express app
15
+ const app = createApp();
16
+
17
+ // Start server
18
+ server = http.createServer(app);
19
+ server.listen(config.port, () => {
20
+ logger.info(
21
+ `Server running on port ${config.port} in ${config.nodeEnv} mode`,
22
+ );
23
+ });
24
+
25
+ // Graceful shutdown handlers
26
+ const shutdown = async (signal: string) => {
27
+ logger.info(`${signal} received, starting graceful shutdown...`);
28
+
29
+ const shutdownTimeout = setTimeout(() => {
30
+ logger.error("Graceful shutdown timeout, forcing exit");
31
+ process.exit(1);
32
+ }, 10000);
33
+
34
+ server.close(async () => {
35
+ clearTimeout(shutdownTimeout);
36
+ await disconnectDatabase();
37
+ logger.info("Server closed gracefully");
38
+ process.exit(0);
39
+ });
40
+ };
41
+
42
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
43
+ process.on("SIGINT", () => shutdown("SIGINT"));
44
+
45
+ process.on("unhandledRejection", (reason, promise) => {
46
+ logger.error("Unhandled Rejection at:", promise, "reason:", reason);
47
+ });
48
+
49
+ process.on("uncaughtException", (error) => {
50
+ logger.error("Uncaught Exception:", error);
51
+ process.exit(1);
52
+ });
53
+ } catch (error: any) {
54
+ logger.error("Failed to start server:", error.message);
55
+ process.exit(1);
56
+ }
57
+ };
@@ -0,0 +1,63 @@
1
+ import { StatusCodes } from "http-status-codes";
2
+
3
+ export class ApiError extends Error {
4
+ statusCode: number;
5
+ isOperational: boolean;
6
+ errors?: Record<string, any> | string[];
7
+
8
+ constructor(
9
+ statusCode: number,
10
+ message: string,
11
+ isOperational: boolean = true,
12
+ errors?: Record<string, any> | string[],
13
+ ) {
14
+ super(message);
15
+ Object.setPrototypeOf(this, ApiError.prototype);
16
+ this.statusCode = statusCode;
17
+ this.isOperational = isOperational;
18
+ this.errors = errors;
19
+ }
20
+
21
+ static badRequest(
22
+ message: string,
23
+ errors?: Record<string, any> | string[],
24
+ ): ApiError {
25
+ return new ApiError(StatusCodes.BAD_REQUEST, message, true, errors);
26
+ }
27
+
28
+ static unauthorized(message: string = "Unauthorized"): ApiError {
29
+ return new ApiError(StatusCodes.UNAUTHORIZED, message);
30
+ }
31
+
32
+ static forbidden(message: string = "Forbidden"): ApiError {
33
+ return new ApiError(StatusCodes.FORBIDDEN, message);
34
+ }
35
+
36
+ static notFound(message: string = "Not found"): ApiError {
37
+ return new ApiError(StatusCodes.NOT_FOUND, message);
38
+ }
39
+
40
+ static conflict(message: string = "Conflict"): ApiError {
41
+ return new ApiError(StatusCodes.CONFLICT, message);
42
+ }
43
+
44
+ static unprocessable(
45
+ message: string = "Unprocessable entity",
46
+ errors?: Record<string, any>,
47
+ ): ApiError {
48
+ return new ApiError(
49
+ StatusCodes.UNPROCESSABLE_ENTITY,
50
+ message,
51
+ true,
52
+ errors,
53
+ );
54
+ }
55
+
56
+ static internal(message: string = "Internal server error"): ApiError {
57
+ return new ApiError(StatusCodes.INTERNAL_SERVER_ERROR, message, false);
58
+ }
59
+
60
+ static serviceUnavailable(message: string = "Service unavailable"): ApiError {
61
+ return new ApiError(StatusCodes.SERVICE_UNAVAILABLE, message, false);
62
+ }
63
+ }
@@ -0,0 +1,7 @@
1
+ import { Request, Response, NextFunction, RequestHandler } from "express";
2
+
3
+ export const asyncHandler = (fn: RequestHandler): RequestHandler => {
4
+ return (req: Request, res: Response, next: NextFunction) => {
5
+ Promise.resolve(fn(req, res, next)).catch(next);
6
+ };
7
+ };
@@ -0,0 +1,102 @@
1
+ import { Response } from "express";
2
+ import { StatusCodes } from "http-status-codes";
3
+
4
+ interface IApiResponse<T> {
5
+ status: "success" | "error" | "info";
6
+ data?: T;
7
+ message?: string;
8
+ errors?: Record<string, any>;
9
+ statusCode?: number;
10
+ }
11
+
12
+ export class ApiResponseHandler {
13
+ static success<T>(
14
+ res: Response,
15
+ data: T,
16
+ message: string = "Success",
17
+ statusCode: number = StatusCodes.OK,
18
+ ): Response {
19
+ return res.status(statusCode).json({
20
+ status: "success",
21
+ data,
22
+ message,
23
+ } as IApiResponse<T>);
24
+ }
25
+
26
+ static created<T>(
27
+ res: Response,
28
+ data: T,
29
+ message: string = "Resource created successfully",
30
+ ): Response {
31
+ return res.status(StatusCodes.CREATED).json({
32
+ status: "success",
33
+ data,
34
+ message,
35
+ } as IApiResponse<T>);
36
+ }
37
+
38
+ static error(
39
+ res: Response,
40
+ message: string,
41
+ statusCode: number = StatusCodes.BAD_REQUEST,
42
+ errors?: Record<string, any> | string[],
43
+ ): Response {
44
+ return res.status(statusCode).json({
45
+ status: "error",
46
+ message,
47
+ errors,
48
+ statusCode,
49
+ } as IApiResponse<null>);
50
+ }
51
+
52
+ static info<T>(
53
+ res: Response,
54
+ message: string,
55
+ data?: T,
56
+ statusCode: number = StatusCodes.OK,
57
+ ): Response {
58
+ return res.status(statusCode).json({
59
+ status: "info",
60
+ message,
61
+ data,
62
+ } as IApiResponse<T>);
63
+ }
64
+
65
+ static noContent(res: Response): Response {
66
+ return res.status(StatusCodes.NO_CONTENT).send();
67
+ }
68
+
69
+ static paginated<T>(
70
+ res: Response,
71
+ data: T[],
72
+ total: number,
73
+ page: number,
74
+ limit: number,
75
+ message: string = "Retrieved successfully",
76
+ ): Response {
77
+ return res.status(StatusCodes.OK).json({
78
+ status: "success",
79
+ data,
80
+ message,
81
+ pagination: {
82
+ total,
83
+ page,
84
+ limit,
85
+ pages: Math.ceil(total / limit),
86
+ },
87
+ });
88
+ }
89
+
90
+ static buildResponse<T>(
91
+ data: T,
92
+ message: string,
93
+ statusCode: number = StatusCodes.OK,
94
+ ): IApiResponse<T> {
95
+ return {
96
+ status: "success",
97
+ data,
98
+ message,
99
+ statusCode,
100
+ };
101
+ }
102
+ }
@@ -0,0 +1,32 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { Schema } from "joi";
3
+ import { ApiError } from "./apiError";
4
+ import { StatusCodes } from "http-status-codes";
5
+
6
+ export const validateWithJoi = (schema: Schema) => {
7
+ return (req: Request, res: Response, next: NextFunction) => {
8
+ const { error, value } = schema.validate(req.body, {
9
+ abortEarly: false,
10
+ stripUnknown: true,
11
+ });
12
+
13
+ if (error) {
14
+ const errors: Record<string, any> = {};
15
+ error.details.forEach((detail) => {
16
+ errors[detail.path.join(".")] = detail.message;
17
+ });
18
+
19
+ return next(
20
+ new ApiError(
21
+ StatusCodes.UNPROCESSABLE_ENTITY,
22
+ "Validation failed",
23
+ true,
24
+ errors,
25
+ ),
26
+ );
27
+ }
28
+
29
+ req.body = value;
30
+ next();
31
+ };
32
+ };
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "paths": {
17
+ "@/*": ["./*"],
18
+ "@modules/*": ["./modules/*"],
19
+ "@config/*": ["./config/*"],
20
+ "@middleware/*": ["./middleware/*"],
21
+ "@utils/*": ["./utils/*"]
22
+ }
23
+ },
24
+ "include": ["src/**/*"],
25
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
26
+ }