├── .gitignore ├── .dockerignore ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── .prettierrc ├── .eslintignore ├── src ├── shared │ ├── response │ │ ├── success │ │ │ ├── success.interface.ts │ │ │ └── success.response.ts │ │ └── failure │ │ │ ├── failure.interface.ts │ │ │ └── failure.response.ts │ ├── error │ │ ├── custom │ │ │ ├── custom.interface.ts │ │ │ └── custom.error.ts │ │ ├── server.error.ts │ │ └── client.error.ts │ └── exception │ │ ├── exception.interface.ts │ │ └── handler.exception.ts ├── app.config.ts ├── module │ ├── health │ │ ├── health.controller.ts │ │ └── health.route.ts │ └── post │ │ ├── post.interface.ts │ │ ├── post.route.ts │ │ ├── post.exception.ts │ │ ├── post.controller.ts │ │ └── post.service.ts ├── database │ └── mongo │ │ ├── mongo.client.ts │ │ └── mongo.database.ts ├── app.exception.ts ├── index.ts ├── app.process.ts ├── app.route.ts ├── app.terminator.ts └── server.ts ├── developer.dockerfile ├── .eslintrc ├── esbuild.js ├── tsconfig.json ├── package.json ├── developer.docker-compose.yml ├── README.md └── express-mongodb.postman_collection.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | 3 | node_modules -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .vscode 3 | dist 4 | node_modules 5 | .gitattributes 6 | .gitignore 7 | README.md 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | dist 3 | node_modules 4 | 5 | .eslintignore 6 | .prettierignore 7 | .prettierrc 8 | 9 | tsconfig.json -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "printWidth": 100, 5 | "singleQuote": false, 6 | "trailingComma": "none" 7 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | coverage 3 | dist 4 | node_modules 5 | 6 | .eslintignore 7 | .prettierignore 8 | .prettierrc 9 | esbuild.js 10 | 11 | tsconfig.json -------------------------------------------------------------------------------- /src/shared/response/success/success.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ISuccessResponse { 2 | statusCode: number; 3 | statusName: string; 4 | payload?: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/error/custom/custom.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IErrorResponse { 2 | statusCode: number; 3 | statusName: string; 4 | payload: IErrorPayload; 5 | } 6 | 7 | export interface IErrorPayload { 8 | errorCode: number; 9 | errorName: string; 10 | errorMessage: string; 11 | errorRawMessage?: unknown; 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/exception/exception.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IErrorResponse { 2 | statusCode: number; 3 | statusName: string; 4 | payload: IErrorPayload; 5 | } 6 | 7 | export interface IErrorPayload { 8 | errorCode: number; 9 | errorName: string; 10 | errorMessage: string; 11 | errorRawMessage?: unknown; 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/response/failure/failure.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IErrorResponse { 2 | statusCode: number; 3 | statusName: string; 4 | payload: IErrorPayload; 5 | } 6 | 7 | export interface IErrorPayload { 8 | errorCode: number; 9 | errorName: string; 10 | errorMessage: string; 11 | errorRawMessage?: unknown; 12 | } 13 | -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, port, str } from "envalid"; 2 | 3 | const appEnvValidate = () => { 4 | cleanEnv(process.env, { 5 | APP_PORT: port(), 6 | MONGO_SERVICE: str(), 7 | MONGO_USERNAME: str(), 8 | MONGO_PASSWORD: str(), 9 | MONGO_PATH: str() 10 | }); 11 | }; 12 | 13 | export { appEnvValidate }; 14 | -------------------------------------------------------------------------------- /developer.dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.14.2-bullseye-slim as builder 2 | 3 | RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app 4 | WORKDIR /home/node/app 5 | USER node 6 | 7 | COPY tsconfig.json tsconfig.json 8 | COPY esbuild.js esbuild.js 9 | COPY package.json package.json 10 | 11 | RUN npm install 12 | COPY --chown=node:node . . -------------------------------------------------------------------------------- /src/module/health/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { SuccessOk } from "@shared/response/success/success.response"; 2 | import { Request, Response } from "express"; 3 | 4 | class HealthCheckController { 5 | getHealth = (request: Request, response: Response): void => { 6 | SuccessOk(response, { status: "healthy" }); 7 | }; 8 | } 9 | 10 | export { HealthCheckController }; 11 | -------------------------------------------------------------------------------- /src/database/mongo/mongo.client.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from "mongodb"; 2 | 3 | const AppMongoClient = async () => { 4 | const { MONGO_SERVICE, MONGO_USERNAME, MONGO_PASSWORD, MONGO_PATH } = process.env; 5 | const mongoURI: string = `${MONGO_SERVICE}://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_PATH}`; 6 | return new MongoClient(mongoURI); 7 | }; 8 | 9 | export { AppMongoClient }; 10 | -------------------------------------------------------------------------------- /src/app.exception.ts: -------------------------------------------------------------------------------- 1 | import { Application, NextFunction, Request, Response } from "express"; 2 | import { HandlerException } from "src/shared/exception/handler.exception"; 3 | 4 | const appException = (app: Application) => { 5 | app.use((error: Error, request: Request, response: Response, next: NextFunction) => { 6 | new HandlerException(error, request, response, next); 7 | }); 8 | }; 9 | 10 | export { appException }; 11 | -------------------------------------------------------------------------------- /src/module/health/health.route.ts: -------------------------------------------------------------------------------- 1 | import { HealthCheckController } from "@module/health/health.controller"; 2 | import { Router } from "express"; 3 | 4 | class HealthCheckRoute { 5 | path = "/health"; 6 | router = Router(); 7 | controller: HealthCheckController; 8 | 9 | constructor() { 10 | this.controller = new HealthCheckController(); 11 | this.router.get(this.path, this.controller.getHealth); 12 | } 13 | } 14 | 15 | export { HealthCheckRoute }; 16 | -------------------------------------------------------------------------------- /src/module/post/post.interface.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "mongodb"; 2 | 3 | interface IPost { 4 | title: string; 5 | author: string; 6 | content: string; 7 | } 8 | 9 | interface IPostCreate extends IPost { 10 | id?: ObjectId; 11 | } 12 | 13 | interface IPostUpdate extends IPost { 14 | id: ObjectId; 15 | } 16 | 17 | interface IPostService { 18 | getPosts(): any; 19 | getPostById(postId: ObjectId): any; 20 | createPost(post: IPostCreate): any; 21 | updatePost(post: IPostUpdate): any; 22 | deletePost(postId: ObjectId): any; 23 | } 24 | 25 | export { IPost, IPostCreate, IPostService, IPostUpdate }; 26 | -------------------------------------------------------------------------------- /src/shared/error/custom/custom.error.ts: -------------------------------------------------------------------------------- 1 | import { IErrorPayload } from "./custom.interface"; 2 | 3 | class CustomError extends Error { 4 | readonly errorCode: number; 5 | readonly errorName: string; 6 | readonly errorMessage: string; 7 | readonly errorRawMessage: unknown; 8 | 9 | constructor({ errorCode, errorName, errorMessage, errorRawMessage }: IErrorPayload) { 10 | super(errorMessage); 11 | 12 | this.errorCode = errorCode; 13 | this.errorName = errorName; 14 | this.errorMessage = errorMessage; 15 | this.errorRawMessage = errorRawMessage; 16 | 17 | Object.setPrototypeOf(this, new.target.prototype); 18 | } 19 | } 20 | 21 | export { CustomError }; 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "express-async-errors"; 2 | import "./app.process"; 3 | 4 | import http from "http"; 5 | import { createHttpTerminator } from "http-terminator"; 6 | import moduleAlias from "module-alias"; 7 | 8 | import Server from "./server"; 9 | 10 | const sourcePath = __dirname; 11 | const moduleAliasPath = { 12 | "@database": `${sourcePath}/database`, 13 | "@module": `${sourcePath}/module`, 14 | "@shared": `${sourcePath}/shared` 15 | }; 16 | 17 | moduleAlias.addAliases(moduleAliasPath); 18 | 19 | export const server = http.createServer(new Server().getServer()); 20 | export const httpTerminator = createHttpTerminator({ server }); 21 | 22 | (async () => new Server().serverListen())(); 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["prettier", "@typescript-eslint", "simple-import-sort"], 5 | "extends": [ 6 | "prettier", 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/no-explicit-any": "off", 13 | "@typescript-eslint/no-unused-vars": "off", 14 | "prettier/prettier": [ 15 | "warn", 16 | { 17 | "printWidth": 100, 18 | "endOfLine": "lf" 19 | } 20 | ], 21 | "simple-import-sort/imports": "error", 22 | "simple-import-sort/exports": "error" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app.process.ts: -------------------------------------------------------------------------------- 1 | import process from "node:process"; 2 | 3 | import { AppTerminator } from "./app.terminator"; 4 | 5 | process.on("EACCES", () => { 6 | console.log(`Process ${process.pid} received EACCES`); 7 | new AppTerminator().handleExit(1); 8 | }); 9 | 10 | process.on("EADDRINUSE", () => { 11 | console.log(`Process ${process.pid} received EADDRINUSE`); 12 | new AppTerminator().handleExit(1); 13 | }); 14 | 15 | process.on("SIGTERM", () => { 16 | console.log(`Process ${process.pid} received SIGTERM`); 17 | new AppTerminator().handleExit(0); 18 | }); 19 | 20 | process.on("SIGINT", () => { 21 | console.log(`Process ${process.pid} received SIGINT`); 22 | new AppTerminator().handleExit(0); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app.route.ts: -------------------------------------------------------------------------------- 1 | import { HealthCheckRoute } from "@module/health/health.route"; 2 | import { PostRoute } from "@module/post/post.route"; 3 | import { BadRequest, NotFound } from "@shared/error/client.error"; 4 | import { Application, Request, Response } from "express"; 5 | 6 | const appModuleRoute = (app: Application) => { 7 | const moduleRoute = () => [new HealthCheckRoute(), new PostRoute()]; 8 | 9 | moduleRoute().forEach((appRoute) => { 10 | app.use("/api", appRoute.router); 11 | }); 12 | }; 13 | 14 | const appDefaultRoute = (app: Application) => { 15 | app.use("*", (request: Request, response: Response) => { 16 | throw new BadRequest(); 17 | }); 18 | }; 19 | 20 | export { appDefaultRoute, appModuleRoute }; 21 | -------------------------------------------------------------------------------- /src/module/post/post.route.ts: -------------------------------------------------------------------------------- 1 | import { PostController } from "@module/post/post.controller"; 2 | import { Router } from "express"; 3 | 4 | class PostRoute { 5 | path = "/v1/post"; 6 | router = Router(); 7 | controller: PostController; 8 | 9 | constructor() { 10 | this.controller = new PostController(); 11 | this.initRoute(); 12 | } 13 | 14 | initRoute(): void { 15 | this.router.post(this.path, this.controller.createPost); 16 | this.router.get(this.path, this.controller.getPosts); 17 | this.router.get(`${this.path}/:id`, this.controller.getPostById); 18 | this.router.patch(`${this.path}/:id`, this.controller.updatePost); 19 | this.router.delete(`${this.path}/:id`, this.controller.deletePost); 20 | } 21 | } 22 | 23 | export { PostRoute }; 24 | -------------------------------------------------------------------------------- /src/app.terminator.ts: -------------------------------------------------------------------------------- 1 | import { AppDatabase } from "@database/mongo/mongo.database"; 2 | 3 | import { httpTerminator, server } from "./index"; 4 | 5 | class AppTerminator { 6 | handleExit = async (code: number): Promise => { 7 | try { 8 | console.log(`Attempting a graceful shutdown with code ${code}`); 9 | if (server.listening) { 10 | new AppDatabase().closeConnection(); 11 | console.log("Terminating HTTP connections"); 12 | await httpTerminator.terminate(); 13 | } 14 | 15 | console.log(`Exiting gracefully with code ${code}`); 16 | process.exit(code); 17 | } catch (error) { 18 | console.log("Error shutting down gracefully"); 19 | console.log(error); 20 | console.log(`Forcing exit with code ${code}`); 21 | process.exit(code); 22 | } 23 | }; 24 | } 25 | 26 | export { AppTerminator }; 27 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const { build, analyzeMetafile } = require("esbuild"); 2 | const fs = require("node:fs"); 3 | const pkg = require("./package.json"); 4 | 5 | appBuild = async () => { 6 | try { 7 | const result = await build({ 8 | entryPoints: ["src/**/*.ts"], 9 | outdir: "dist", 10 | minify: true, 11 | platform: "node", 12 | format: "cjs", 13 | treeShaking: true, 14 | bundle: true, 15 | metafile: true, 16 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})] 17 | }); 18 | 19 | if (result.metafile) { 20 | fs.writeFileSync("./dist/metafile.json", JSON.stringify(result.metafile)); 21 | } 22 | console.log("Build successful:", await analyzeMetafile(result.metafile)); 23 | process.exit(0); 24 | } catch (error) { 25 | console.error("Build failed:", error); 26 | process.exit(1); 27 | } 28 | }; 29 | 30 | appBuild(); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "types": [ 5 | "node" 6 | ], 7 | "lib": [ 8 | "es2022" 9 | ], 10 | "module": "CommonJS", 11 | "resolveJsonModule": true, 12 | "allowJs": true, 13 | "outDir": "dist", 14 | "baseUrl": ".", 15 | "alwaysStrict": true, 16 | "esModuleInterop": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "strict": true, 19 | "noImplicitAny": true, 20 | "moduleResolution": "Node", 21 | "experimentalDecorators": true, 22 | "emitDecoratorMetadata": true, 23 | "paths": { 24 | "@database/*": [ 25 | "src/database/*" 26 | ], 27 | "@module/*": [ 28 | "src/module/*" 29 | ], 30 | "@shared/*": [ 31 | "src/shared/*" 32 | ] 33 | } 34 | }, 35 | "include": [ 36 | "src/**/*.ts" 37 | ], 38 | "exclude": [ 39 | "dist", 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /src/shared/error/server.error.ts: -------------------------------------------------------------------------------- 1 | import status from "http-status"; 2 | 3 | import { CustomError } from "./custom/custom.error"; 4 | 5 | export class InternalServeError extends CustomError { 6 | constructor(errorRawMessage?: unknown) { 7 | super({ 8 | errorCode: Number(status["INTERNAL_SERVER_ERROR"]), 9 | errorName: String(status[`${status.INTERNAL_SERVER_ERROR}_NAME`]), 10 | errorMessage: String(status[`${status.INTERNAL_SERVER_ERROR}_MESSAGE`]), 11 | errorRawMessage: errorRawMessage 12 | }); 13 | } 14 | } 15 | 16 | export class NotImplemented extends CustomError { 17 | constructor(errorRawMessage?: unknown) { 18 | super({ 19 | errorCode: Number(status["INTERNAL_SERVER_ERROR"]), 20 | errorName: String(status[`${status.INTERNAL_SERVER_ERROR}_NAME`]), 21 | errorMessage: String(status[`${status.INTERNAL_SERVER_ERROR}_MESSAGE`]), 22 | errorRawMessage: errorRawMessage 23 | }); 24 | } 25 | } 26 | 27 | NotImplemented; 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "window.zoomLevel": 0, 3 | "files.eol": "\n", 4 | "files.autoSave": "onFocusChange", 5 | "editor.formatOnSave": true, 6 | "editor.formatOnPaste": true, 7 | "[javascript][typescript][json]": { 8 | "editor.tabSize": 2, 9 | "editor.tabCompletion": "on", 10 | "editor.defaultFormatter": "esbenp.prettier-vscode", 11 | }, 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll.eslint": "explicit", 14 | "source.sortImports": "explicit" 15 | }, 16 | "editor.wordWrap": "on", 17 | "editor.insertSpaces": true, 18 | "editor.detectIndentation": false, 19 | 20 | "prettier.tabWidth": 2, 21 | "prettier.semi": true, 22 | "prettier.printWidth": 100, 23 | "prettier.singleQuote": false, 24 | "prettier.trailingComma": "none", 25 | 26 | "javascript.suggestionActions.enabled": false, 27 | 28 | "terminal.integrated.defaultProfile.windows": "GitBash", 29 | "terminal.integrated.profiles.windows": { 30 | "GitBash": { 31 | "source": "Git Bash", 32 | "path": ["C:\\Program Files\\Git\\bin\\bash.exe"], 33 | "icon": "terminal-bash" 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/shared/error/client.error.ts: -------------------------------------------------------------------------------- 1 | import status from "http-status"; 2 | 3 | import { CustomError } from "./custom/custom.error"; 4 | 5 | export class NotModified extends CustomError { 6 | constructor(errorRawMessage?: unknown) { 7 | super({ 8 | errorCode: Number(status["NOT_MODIFIED"]), 9 | errorName: String(status[`${status.NOT_MODIFIED}_NAME`]), 10 | errorMessage: String(status[`${status.NOT_MODIFIED}_MESSAGE`]), 11 | errorRawMessage: errorRawMessage 12 | }); 13 | } 14 | } 15 | 16 | export class NotFound extends CustomError { 17 | constructor(errorRawMessage?: unknown) { 18 | super({ 19 | errorCode: Number(status["NOT_FOUND"]), 20 | errorName: String(status[`${status.NOT_FOUND}_NAME`]), 21 | errorMessage: String(status[`${status.NOT_FOUND}_MESSAGE`]), 22 | errorRawMessage: errorRawMessage 23 | }); 24 | } 25 | } 26 | 27 | export class BadRequest extends CustomError { 28 | constructor(errorRawMessage?: unknown) { 29 | super({ 30 | errorCode: Number(status["BAD_REQUEST"]), 31 | errorName: String(status[`${status.BAD_REQUEST}_NAME`]), 32 | errorMessage: String(status[`${status.BAD_REQUEST}_MESSAGE`]), 33 | errorRawMessage: errorRawMessage 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/shared/response/failure/failure.response.ts: -------------------------------------------------------------------------------- 1 | import { InternalServeError } from "@shared/error/server.error"; 2 | import { Response } from "express"; 3 | 4 | import { IErrorPayload, IErrorResponse } from "./failure.interface"; 5 | 6 | const sanitizeErrorResponse = (errorResponse: IErrorResponse) => { 7 | const { payload } = errorResponse; 8 | const { errorRawMessage } = payload || {}; 9 | 10 | if ( 11 | errorRawMessage && 12 | typeof errorRawMessage === "object" && 13 | errorRawMessage instanceof InternalServeError 14 | ) { 15 | errorResponse.payload = { ...errorRawMessage }; 16 | } 17 | }; 18 | 19 | const failureResponse = (error: IErrorPayload, response: Response): Response => { 20 | const { errorCode, errorName, errorMessage, errorRawMessage } = error; 21 | 22 | const payload = { 23 | errorCode, 24 | errorName, 25 | errorMessage, 26 | ...(errorRawMessage ? { errorRawMessage } : {}) 27 | }; 28 | 29 | const errorResponse: IErrorResponse = { 30 | statusCode: errorCode, 31 | statusName: errorName, 32 | payload 33 | }; 34 | 35 | sanitizeErrorResponse(errorResponse); 36 | 37 | return response.status(errorCode).json(errorResponse); 38 | }; 39 | 40 | export { failureResponse }; 41 | -------------------------------------------------------------------------------- /src/database/mongo/mongo.database.ts: -------------------------------------------------------------------------------- 1 | import { InternalServeError } from "@shared/error/server.error"; 2 | import { MongoClient } from "mongodb"; 3 | 4 | import { AppMongoClient } from "./mongo.client"; 5 | 6 | class AppDatabase { 7 | listDatabases = async (client: MongoClient) => { 8 | const databasesList = await client.db().admin().listDatabases(); 9 | console.log("Databases:"); 10 | databasesList.databases.forEach((db) => console.log(` - ${db.name}`)); 11 | }; 12 | 13 | openConnection = async () => { 14 | const client: MongoClient = await AppMongoClient(); 15 | try { 16 | await client.connect(); 17 | console.log("Connect to the MongoDB cluster"); 18 | await this.listDatabases(client); 19 | } catch (error: unknown) { 20 | console.log("Error connect to the MongoDB cluster"); 21 | throw new InternalServeError(error); 22 | } finally { 23 | await client.close(); 24 | console.log("Close connect to the MongoDB cluster"); 25 | } 26 | }; 27 | 28 | closeConnection = async () => { 29 | const client: MongoClient = await AppMongoClient(); 30 | try { 31 | await client.close(); 32 | console.log("Close connect to the MongoDB cluster"); 33 | } catch (error: unknown) { 34 | throw new InternalServeError(error); 35 | } 36 | }; 37 | } 38 | 39 | export { AppDatabase }; 40 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { AppDatabase } from "@database/mongo/mongo.database"; 2 | import cors from "cors"; 3 | import express from "express"; 4 | 5 | import { appEnvValidate } from "./app.config"; 6 | import { appException } from "./app.exception"; 7 | import { appDefaultRoute, appModuleRoute } from "./app.route"; 8 | 9 | class Server { 10 | serverPort = Number(process.env.APP_PORT); 11 | app: express.Application; 12 | 13 | constructor() { 14 | this.app = express(); 15 | 16 | this.initEnvironment(); 17 | this.initMiddlewares(); 18 | this.initRoutes(); 19 | this.initException(); 20 | } 21 | 22 | initEnvironment() { 23 | appEnvValidate(); 24 | } 25 | 26 | initMiddlewares() { 27 | this.app.use(cors()); 28 | this.app.use(express.json()); 29 | this.app.use(express.urlencoded({ extended: true })); 30 | } 31 | 32 | initRoutes() { 33 | appModuleRoute(this.app); 34 | appDefaultRoute(this.app); 35 | } 36 | 37 | initException() { 38 | appException(this.app); 39 | } 40 | 41 | getServer() { 42 | return this.app; 43 | } 44 | 45 | serverListen() { 46 | return this.getServer().listen(Number(this.serverPort), async () => { 47 | new AppDatabase().openConnection(); 48 | console.log(`App port : ${this.serverPort}`); 49 | console.log(`App environment : ${process.env.ENV_NAME}`); 50 | }); 51 | } 52 | } 53 | 54 | export default Server; 55 | -------------------------------------------------------------------------------- /src/shared/exception/handler.exception.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | import { CustomError } from "../error/custom/custom.error"; 4 | import { InternalServeError } from "../error/server.error"; 5 | import { failureResponse } from "../response/failure/failure.response"; 6 | 7 | class HandlerException { 8 | constructor(error: Error, request: Request, response: Response, next: NextFunction) { 9 | if (this.isTrustedError(error)) { 10 | this.trustedError(error as CustomError, response); 11 | } else { 12 | this.untrustedError(error, response); 13 | } 14 | } 15 | 16 | isTrustedError(error: Error): boolean { 17 | return error instanceof CustomError; 18 | } 19 | 20 | normalizeError(error: Error | string | object): Error { 21 | if (error instanceof Error) { 22 | return error; 23 | } 24 | if (typeof error === "string") { 25 | return new Error(error); 26 | } 27 | return new Error(JSON.stringify(error)); 28 | } 29 | 30 | trustedError(error: CustomError, response: Response): void { 31 | failureResponse(error, response); 32 | } 33 | 34 | untrustedError(error: Error, response: Response): void { 35 | const normalizedError = this.normalizeError(error); 36 | const serialized = normalizedError.message; 37 | const internalError = new InternalServeError(serialized); 38 | failureResponse(internalError as CustomError, response); 39 | } 40 | } 41 | 42 | export { HandlerException }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-mongodb", 3 | "version": "1.0.0", 4 | "description": "express + typescript + mongodb", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "app:watch:dev": "cross-env ENV_NAME=DEV tsx watch src", 8 | "app:format": "prettier --config .prettierrc src/**/*.ts --write", 9 | "app:typecheck": "tsc -noEmit", 10 | "app:lint": "eslint . --ext .ts", 11 | "app:build": "rm -rf ./dist && node esbuild.js", 12 | "app:start:dev": "cross-env ENV_NAME=DEV node dist/index.js" 13 | }, 14 | "keywords": [], 15 | "author": "Sudhakar Jonnakuti", 16 | "license": "ISC", 17 | "dependencies": { 18 | "cors": "^2.8.5", 19 | "envalid": "^8.0.0", 20 | "express": "^4.18.2", 21 | "express-async-errors": "^3.1.1", 22 | "express-mongo-sanitize": "^2.2.0", 23 | "http-status": "^1.7.3", 24 | "http-terminator": "^3.2.0", 25 | "mongodb": "^6.3.0" 26 | }, 27 | "devDependencies": { 28 | "@types/cors": "^2.8.17", 29 | "@types/express": "^4.17.21", 30 | "@types/module-alias": "^2.0.4", 31 | "@types/node": "^20.10.6", 32 | "@typescript-eslint/eslint-plugin": "^6.17.0", 33 | "@typescript-eslint/parser": "^6.17.0", 34 | "cross-env": "^7.0.3", 35 | "esbuild": "^0.19.11", 36 | "eslint": "^8.56.0", 37 | "eslint-config-prettier": "^9.1.0", 38 | "eslint-plugin-prettier": "^5.1.2", 39 | "eslint-plugin-simple-import-sort": "^10.0.0", 40 | "module-alias": "^2.2.3", 41 | "prettier": "^3.1.1", 42 | "rimraf": "^5.0.5", 43 | "tsx": "^4.7.0", 44 | "typescript": "^5.3.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/module/post/post.exception.ts: -------------------------------------------------------------------------------- 1 | import { BadRequest, NotFound, NotModified } from "@shared/error/client.error"; 2 | import { InternalServeError, NotImplemented } from "@shared/error/server.error"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | const getPostsException = (error: unknown) => { 6 | throw new InternalServeError(error); 7 | }; 8 | 9 | const getPostByIdException = (postId: ObjectId, error: unknown) => { 10 | if (error instanceof NotFound) { 11 | throw new NotFound(`The post with the id "${postId}" not found.`); 12 | } else { 13 | throw new InternalServeError(error); 14 | } 15 | }; 16 | 17 | const createPostException = (error: unknown) => { 18 | if (error instanceof NotImplemented) { 19 | throw new NotImplemented(`Failed to create a new post.`); 20 | } else { 21 | throw new InternalServeError(error); 22 | } 23 | }; 24 | 25 | const updatePostException = (postId: ObjectId, error: unknown) => { 26 | if (error instanceof NotFound) { 27 | throw new NotFound(`The post with the id "${postId}" not found.`); 28 | } else if (error instanceof NotModified) { 29 | throw new NotModified(`Post with id: ${postId} not updated`); 30 | } else { 31 | throw new InternalServeError(error); 32 | } 33 | }; 34 | 35 | const deletePostException = (postId: ObjectId, error: unknown) => { 36 | if (error instanceof NotFound) { 37 | throw new NotFound(`The post with the id "${postId}" not found.`); 38 | } else if (error instanceof BadRequest) { 39 | throw new BadRequest(`Failed to remove post with id ${postId}`); 40 | } else { 41 | throw new InternalServeError(error); 42 | } 43 | }; 44 | 45 | export { 46 | createPostException, 47 | deletePostException, 48 | getPostByIdException, 49 | getPostsException, 50 | updatePostException 51 | }; 52 | -------------------------------------------------------------------------------- /developer.docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mongodb: 5 | image: mongo 6 | container_name: mongodb 7 | restart: always 8 | ports: 9 | - "27017:27017" 10 | environment: 11 | MONGO_INITDB_ROOT_USERNAME: admin 12 | MONGO_INITDB_ROOT_PASSWORD: pass 13 | volumes: 14 | - mongo-volume:/data/db 15 | - mongo-config-volume:/data/configdb 16 | networks: 17 | - app-network 18 | 19 | mongo-express: 20 | depends_on: 21 | - mongodb 22 | image: mongo-express 23 | container_name: mongo-express 24 | restart: always 25 | ports: 26 | - "8084:8081" 27 | expose: 28 | - "8084" 29 | environment: 30 | ME_CONFIG_OPTIONS_EDITORTHEME: ambiance 31 | ME_CONFIG_MONGODB_ADMINUSERNAME: admin 32 | ME_CONFIG_MONGODB_ADMINPASSWORD: pass 33 | ME_CONFIG_MONGODB_SERVER: mongodb 34 | networks: 35 | - app-network 36 | 37 | express-mongodb: 38 | depends_on: 39 | - mongodb 40 | build: 41 | context: . 42 | dockerfile: developer.dockerfile 43 | image: express-mongodb 44 | container_name: express-mongodb 45 | ports: 46 | - "5000:5000" 47 | environment: 48 | APP_PORT: 5000 49 | MONGO_SERVICE: mongodb 50 | MONGO_USERNAME: admin 51 | MONGO_PASSWORD: pass 52 | MONGO_PATH: mongodb:27017 53 | volumes: 54 | - .:/home/node/app 55 | - node_modules:/home/node/app/node_modules 56 | command: > 57 | bash -c " 58 | npm run app:typecheck 59 | npm run app:lint 60 | npm run app:build 61 | npm run app:start:dev 62 | " 63 | restart: unless-stopped 64 | networks: 65 | - app-network 66 | 67 | networks: 68 | app-network: 69 | driver: bridge 70 | 71 | volumes: 72 | mongo-volume: 73 | mongo-config-volume: 74 | node_modules: 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-mongodb 2 | 3 | ``` 4 | Up application: 5 | docker-compose -f developer.docker-compose.yml up -d 6 | 7 | Down application: 8 | docker-compose -f developer.docker-compose.yml down -v --rmi all 9 | 10 | Steps to login in mongo-express: 11 | 12 | 1. Open mongo-express in the web browser by visiting http://localhost:8084. 13 | 2. To log in, fill in the following details 14 | > Username : admin 15 | > Password : pass 16 | 17 | What are the drawbacks of Mongoose? 18 | 19 | On the downside, learning mongoose can take some time, and has some limitations in handling schemas that are quite complex. 20 | However, if your collection schema is unpredictable, or you want a Mongo-shell like experience inside Node. js, 21 | then go ahead and use the mongodb driver. It is the simplest to pick up. 22 | 23 | What is NoSQL injection ? 24 | 25 | It's a vulnerability where an attacker is able to interfere with the queries that an application makes to a NoSQL database. 26 | NoSQL injection may enable an attacker to: 27 | 1. Bypass authentication or protection mechanisms. 28 | 2. Extract or edit data. 29 | 3. Cause a denial of service. 30 | 4. Execute code on the server 31 | 32 | Reference: 33 | 34 | MongoDB and Node.js Tutorial - CRUD Operations 35 | https://www.mongodb.com/developer/languages/javascript/node-crud-tutorial/#create 36 | https://github.com/mongodb-developer/nodejs-quickstart/blob/master/create.js 37 | 38 | How to Use TypeScript with MongoDB Atlas 39 | https://www.mongodb.com/compatibility/using-typescript-with-mongodb-tutorial 40 | 41 | NoSQL, MongoDB, Mongo Express, and Docker 42 | https://medium.com/javarevisited/nosql-mongodb-mongo-express-and-docker-d6a4355ff395 43 | 44 | Express.JS + mongoDB + mongo-express on the docker compose 45 | https://qiita.com/iwasaki-hub/items/cc1d9bffd382f6ccd5db 46 | 47 | NoSQL injection 48 | https://medium.com/@BhaktiKhedkar/nosql-injection-558337ea7d6c 49 | ``` 50 | -------------------------------------------------------------------------------- /src/shared/response/success/success.response.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import status from "http-status"; 3 | 4 | import { ISuccessResponse } from "./success.interface"; 5 | 6 | const SuccessOk = (response: Response, payload?: unknown): Response => { 7 | const statusOk: ISuccessResponse = { 8 | statusCode: Number(status["OK"]), 9 | statusName: String(status[`${status.OK}_NAME`]), 10 | ...(payload ? { payload } : {}) 11 | }; 12 | 13 | return response.status(statusOk.statusCode).json(statusOk); 14 | }; 15 | 16 | const SuccessCreated = (response: Response, payload?: unknown): Response => { 17 | const statusCreated: ISuccessResponse = { 18 | statusCode: Number(status["CREATED"]), 19 | statusName: String(status[`${status.OK}_CREATED`]), 20 | ...(payload ? { payload } : {}) 21 | }; 22 | 23 | return response.status(statusCreated.statusCode).json(statusCreated); 24 | }; 25 | 26 | const SuccessAccepted = (response: Response, payload?: unknown): Response => { 27 | const statusAccepted: ISuccessResponse = { 28 | statusCode: Number(status["ACCEPTED"]), 29 | statusName: String(status[`${status.OK}_ACCEPTED`]), 30 | ...(payload ? { payload } : {}) 31 | }; 32 | 33 | return response.status(statusAccepted.statusCode).json(statusAccepted); 34 | }; 35 | 36 | const SuccessNonAuthInfo = (response: Response, payload?: unknown): Response => { 37 | const statusNonAuthInfo: ISuccessResponse = { 38 | statusCode: Number(status["NON_AUTHORITATIVE_INFORMATION"]), 39 | statusName: String(status[`${status.OK}_NON_AUTHORITATIVE_INFORMATION`]), 40 | ...(payload ? { payload } : {}) 41 | }; 42 | 43 | return response.status(statusNonAuthInfo.statusCode).json(statusNonAuthInfo); 44 | }; 45 | 46 | const SuccessNoContent = (response: Response, payload?: unknown): Response => { 47 | const statusNonAuthInfo: ISuccessResponse = { 48 | statusCode: Number(status["NO_CONTENT"]), 49 | statusName: String(status[`${status.OK}_NO_CONTENT`]), 50 | ...(payload ? { payload } : {}) 51 | }; 52 | 53 | return response.status(statusNonAuthInfo.statusCode).json(statusNonAuthInfo); 54 | }; 55 | 56 | export { SuccessAccepted, SuccessCreated, SuccessNoContent, SuccessNonAuthInfo, SuccessOk }; 57 | -------------------------------------------------------------------------------- /src/module/post/post.controller.ts: -------------------------------------------------------------------------------- 1 | import { PostService } from "@module/post/post.service"; 2 | import { SuccessOk } from "@shared/response/success/success.response"; 3 | import { Request, Response } from "express"; 4 | import { ObjectId } from "mongodb"; 5 | 6 | import { 7 | createPostException, 8 | deletePostException, 9 | getPostByIdException, 10 | getPostsException, 11 | updatePostException 12 | } from "./post.exception"; 13 | 14 | class PostController { 15 | postService: PostService; 16 | 17 | constructor() { 18 | this.postService = new PostService(); 19 | } 20 | 21 | getPosts = async (request: Request, response: Response) => { 22 | try { 23 | const resultGetPosts = await this.postService.getPosts(); 24 | SuccessOk(response, resultGetPosts); 25 | } catch (error: unknown) { 26 | getPostsException(error); 27 | } 28 | }; 29 | 30 | getPostById = async (request: Request, response: Response) => { 31 | const id = request?.params?.id; 32 | const mongoId = new ObjectId(id); 33 | 34 | try { 35 | const resultGetPosts = await this.postService.getPostById(mongoId); 36 | SuccessOk(response, resultGetPosts); 37 | } catch (error: unknown) { 38 | getPostByIdException(mongoId, error); 39 | } 40 | }; 41 | 42 | createPost = async (request: Request, response: Response) => { 43 | const postInput = request.body; 44 | 45 | try { 46 | const resultCreatedPost = await this.postService.createPost(postInput); 47 | SuccessOk(response, resultCreatedPost); 48 | } catch (error: unknown) { 49 | createPostException(error); 50 | } 51 | }; 52 | 53 | updatePost = async (request: Request, response: Response) => { 54 | const id = request?.params?.id; 55 | const postInput = request.body; 56 | postInput.id = new ObjectId(id); 57 | 58 | try { 59 | await this.postService.getPostById(postInput.id); 60 | const resultUpdatePost = await this.postService.updatePost(postInput); 61 | SuccessOk(response, resultUpdatePost); 62 | } catch (error: unknown) { 63 | updatePostException(postInput.id, error); 64 | } 65 | }; 66 | 67 | deletePost = async (request: Request, response: Response) => { 68 | const id = request?.params?.id; 69 | const mongoId = new ObjectId(id); 70 | 71 | try { 72 | await this.postService.getPostById(mongoId); 73 | const resultDeletePost = await this.postService.deletePost(mongoId); 74 | SuccessOk(response, resultDeletePost); 75 | } catch (error: unknown) { 76 | deletePostException(mongoId, error); 77 | } 78 | }; 79 | } 80 | 81 | export { PostController }; 82 | -------------------------------------------------------------------------------- /express-mongodb.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "a11ce473-0fb7-48e9-8725-d8633ab40323", 4 | "name": "express-mongodb", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "6924403" 7 | }, 8 | "item": [ 9 | { 10 | "name": "00. health - /api/health", 11 | "request": { 12 | "method": "GET", 13 | "header": [], 14 | "url": { 15 | "raw": "http://localhost:5000/api/health", 16 | "protocol": "http", 17 | "host": [ 18 | "localhost" 19 | ], 20 | "port": "5000", 21 | "path": [ 22 | "api", 23 | "health" 24 | ] 25 | } 26 | }, 27 | "response": [] 28 | }, 29 | { 30 | "name": "01. createPost - /api/v1/post", 31 | "request": { 32 | "method": "POST", 33 | "header": [], 34 | "body": { 35 | "mode": "raw", 36 | "raw": "{\r\n \"title\": \"1984\",\r\n \"author\": \"George Orwell\",\r\n \"content\": \"1984 tells the futuristic story of a dystopian.\"\r\n}", 37 | "options": { 38 | "raw": { 39 | "language": "json" 40 | } 41 | } 42 | }, 43 | "url": { 44 | "raw": "http://localhost:5000/api/v1/post", 45 | "protocol": "http", 46 | "host": [ 47 | "localhost" 48 | ], 49 | "port": "5000", 50 | "path": [ 51 | "api", 52 | "v1", 53 | "post" 54 | ] 55 | } 56 | }, 57 | "response": [] 58 | }, 59 | { 60 | "name": "02. getPosts - /api/v1/post", 61 | "request": { 62 | "method": "GET", 63 | "header": [], 64 | "url": { 65 | "raw": "http://localhost:5000/api/v1/post", 66 | "protocol": "http", 67 | "host": [ 68 | "localhost" 69 | ], 70 | "port": "5000", 71 | "path": [ 72 | "api", 73 | "v1", 74 | "post" 75 | ] 76 | } 77 | }, 78 | "response": [] 79 | }, 80 | { 81 | "name": "03. getPostById - /api/v1/post/1", 82 | "request": { 83 | "method": "GET", 84 | "header": [], 85 | "url": { 86 | "raw": "http://localhost:5000/api/v1/post/659462005ad97a3e4ac1c2b7", 87 | "protocol": "http", 88 | "host": [ 89 | "localhost" 90 | ], 91 | "port": "5000", 92 | "path": [ 93 | "api", 94 | "v1", 95 | "post", 96 | "659462005ad97a3e4ac1c2b7" 97 | ] 98 | } 99 | }, 100 | "response": [] 101 | }, 102 | { 103 | "name": "04. updatePost - /api/v1/post/1", 104 | "request": { 105 | "method": "PATCH", 106 | "header": [], 107 | "body": { 108 | "mode": "raw", 109 | "raw": "{\r\n \"title\": \"The 1984\",\r\n \"author\": \"George Orwell\",\r\n \"content\": \"1984 tells the futuristic story of a dystopian.\"\r\n}", 110 | "options": { 111 | "raw": { 112 | "language": "json" 113 | } 114 | } 115 | }, 116 | "url": { 117 | "raw": "http://localhost:5000/api/v1/post/659462005ad97a3e4ac1c2b7", 118 | "protocol": "http", 119 | "host": [ 120 | "localhost" 121 | ], 122 | "port": "5000", 123 | "path": [ 124 | "api", 125 | "v1", 126 | "post", 127 | "659462005ad97a3e4ac1c2b7" 128 | ] 129 | } 130 | }, 131 | "response": [] 132 | }, 133 | { 134 | "name": "05.deletePost - /api/v1/post/1", 135 | "request": { 136 | "method": "DELETE", 137 | "header": [], 138 | "url": { 139 | "raw": "http://localhost:5000/api/v1/post/659462005ad97a3e4ac1c2b7", 140 | "protocol": "http", 141 | "host": [ 142 | "localhost" 143 | ], 144 | "port": "5000", 145 | "path": [ 146 | "api", 147 | "v1", 148 | "post", 149 | "659462005ad97a3e4ac1c2b7" 150 | ] 151 | } 152 | }, 153 | "response": [] 154 | } 155 | ] 156 | } -------------------------------------------------------------------------------- /src/module/post/post.service.ts: -------------------------------------------------------------------------------- 1 | import { AppMongoClient } from "@database/mongo/mongo.client"; 2 | import { IPostCreate, IPostService, IPostUpdate } from "@module/post/post.interface"; 3 | import { BadRequest, NotFound, NotModified } from "@shared/error/client.error"; 4 | import { NotImplemented } from "@shared/error/server.error"; 5 | import mongoSanitize from "express-mongo-sanitize"; 6 | import { MongoClient, ObjectId } from "mongodb"; 7 | 8 | import { 9 | createPostException, 10 | deletePostException, 11 | getPostByIdException, 12 | getPostsException, 13 | updatePostException 14 | } from "./post.exception"; 15 | class PostService implements IPostService { 16 | getPosts = async () => { 17 | const client: MongoClient = await AppMongoClient(); 18 | 19 | try { 20 | await client.connect(); 21 | console.log("Connect to the MongoDB cluster"); 22 | const database = client.db("blog"); 23 | const collection = database.collection("post"); 24 | return await collection.find({}).toArray(); 25 | } catch (error: unknown) { 26 | getPostsException(error); 27 | } finally { 28 | await client.close(); 29 | console.log("Close connect to the MongoDB cluster"); 30 | } 31 | }; 32 | 33 | getPostById = async (postId: ObjectId) => { 34 | const client: MongoClient = await AppMongoClient(); 35 | const query = { _id: new ObjectId(postId) }; 36 | try { 37 | await client.connect(); 38 | console.log("Connect to the MongoDB cluster"); 39 | const database = client.db("blog"); 40 | const collection = database.collection("post"); 41 | mongoSanitize.sanitize(query); 42 | const resultGetPostById = await collection.findOne(query); 43 | if (resultGetPostById) { 44 | return resultGetPostById; 45 | } else { 46 | throw new NotFound(`The post with the id "${postId}" not found.`); 47 | } 48 | } catch (error: unknown) { 49 | getPostByIdException(postId, error); 50 | } finally { 51 | await client.close(); 52 | console.log("Close connect to the MongoDB cluster"); 53 | } 54 | }; 55 | 56 | createPost = async (post: IPostCreate) => { 57 | const client: MongoClient = await AppMongoClient(); 58 | try { 59 | await client.connect(); 60 | console.log("Connect to the MongoDB cluster"); 61 | const database = client.db("blog"); 62 | const collection = database.collection("post"); 63 | mongoSanitize.sanitize({ ...post }); 64 | const result = await collection.insertOne({ ...post }); 65 | if (result && result.insertedId) { 66 | return { id: result.insertedId, message: `Created a post with id ${result.insertedId}` }; 67 | } else { 68 | throw new NotImplemented(`Failed to create a new post.`); 69 | } 70 | } catch (error: unknown) { 71 | createPostException(error); 72 | } finally { 73 | await client.close(); 74 | console.log("Close connect to the MongoDB cluster"); 75 | } 76 | }; 77 | 78 | updatePost = async (post: IPostUpdate) => { 79 | const client: MongoClient = await AppMongoClient(); 80 | const query = { _id: new ObjectId(post.id) }; 81 | const updatedValues = { 82 | $set: { 83 | title: post.title, 84 | author: post.author, 85 | content: post.content 86 | } 87 | }; 88 | 89 | try { 90 | await client.connect(); 91 | console.log("Connect to the MongoDB cluster"); 92 | const database = client.db("blog"); 93 | const collection = database.collection("post"); 94 | mongoSanitize.sanitize(query); 95 | const resultUpdatePost = await collection.updateOne(query, updatedValues); 96 | if (resultUpdatePost) { 97 | return { id: post.id, message: `Updated post with id ${post.id}` }; 98 | } else { 99 | throw new NotModified(`Post with id: ${post.id} not updated`); 100 | } 101 | } catch (error: unknown) { 102 | updatePostException(post.id, error); 103 | } finally { 104 | await client.close(); 105 | console.log("Close connect to the MongoDB cluster"); 106 | } 107 | }; 108 | 109 | deletePost = async (postId: ObjectId) => { 110 | const client: MongoClient = await AppMongoClient(); 111 | const query = { _id: new ObjectId(postId) }; 112 | try { 113 | await client.connect(); 114 | console.log("Connect to the MongoDB cluster"); 115 | const database = client.db("blog"); 116 | const collection = database.collection("post"); 117 | mongoSanitize.sanitize(query); 118 | const resultDeletePost = await collection.deleteOne(query); 119 | if (resultDeletePost && resultDeletePost.deletedCount) { 120 | return { id: postId, message: `Removed post with id ${postId}` }; 121 | } else if (!resultDeletePost) { 122 | throw new BadRequest(`Failed to remove post with id ${postId}`); 123 | } else if (!resultDeletePost.deletedCount) { 124 | throw new NotFound(`The post with the id "${postId}" not found.`); 125 | } 126 | } catch (error: unknown) { 127 | deletePostException(postId, error); 128 | } finally { 129 | await client.close(); 130 | console.log("Close connect to the MongoDB cluster"); 131 | } 132 | }; 133 | } 134 | 135 | export { PostService }; 136 | --------------------------------------------------------------------------------