├── .gitignore ├── .dockerignore ├── .prettierignore ├── src ├── module │ ├── currency │ │ ├── currency.interface.ts │ │ ├── currency.exception.ts │ │ ├── currency.route.ts │ │ ├── currency.controller.ts │ │ └── currency.service.ts │ └── health │ │ ├── health.controller.ts │ │ └── health.route.ts ├── 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 ├── database │ └── redis │ │ ├── redis.config.ts │ │ └── redis.database.ts ├── app.config.ts ├── app.exception.ts ├── index.ts ├── app.process.ts ├── app.route.ts ├── app.terminator.ts └── server.ts ├── .eslintignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── developer.dockerfile ├── .eslintrc ├── esbuild.js ├── tsconfig.json ├── express-ioredis.postman_collection.json ├── package.json ├── developer.docker-compose.yml └── README.md /.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 -------------------------------------------------------------------------------- /src/module/currency/currency.interface.ts: -------------------------------------------------------------------------------- 1 | interface ICurrencyService { 2 | getCurrency(): any; 3 | } 4 | 5 | export { ICurrencyService }; 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | coverage 3 | dist 4 | node_modules 5 | 6 | .eslintignore 7 | .prettierignore 8 | .prettierrc 9 | esbuild.js 10 | 11 | tsconfig.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "printWidth": 100, 5 | "endOfLine": "lf", 6 | "singleQuote": false, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /src/shared/response/success/success.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ISuccessResponse { 2 | statusCode: number; 3 | statusName: string; 4 | payload?: any; 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "christian-kohler.npm-intellisense" 6 | ] 7 | } -------------------------------------------------------------------------------- /src/database/redis/redis.config.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | 3 | export const appRedis = new Redis({ 4 | port: Number(process.env.REDIS_PORT), 5 | host: process.env.REDIS_HOST 6 | }); 7 | -------------------------------------------------------------------------------- /src/module/currency/currency.exception.ts: -------------------------------------------------------------------------------- 1 | import { InternalServeError } from "@shared/error/server.error"; 2 | 3 | const getCurrencyException = (error: unknown) => { 4 | throw new InternalServeError(error); 5 | }; 6 | 7 | export { getCurrencyException }; 8 | -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, port, str } from "envalid"; 2 | 3 | const appEnvValidate = () => { 4 | cleanEnv(process.env, { 5 | APP_PORT: port(), 6 | REDIS_PORT: port(), 7 | REDIS_HOST: str() 8 | }); 9 | }; 10 | 11 | export { appEnvValidate }; 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/currency/currency.route.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyController } from "@module/currency/currency.controller"; 2 | import { Router } from "express"; 3 | 4 | class CurrencyRoute { 5 | path = "/v1/currency"; 6 | router = Router(); 7 | controller: CurrencyController; 8 | 9 | constructor() { 10 | this.controller = new CurrencyController(); 11 | this.initRoute(); 12 | } 13 | 14 | initRoute(): void { 15 | this.router.get(this.path, this.controller.getCurrency); 16 | } 17 | } 18 | 19 | export { CurrencyRoute }; 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "prettier", 6 | "@typescript-eslint", 7 | "simple-import-sort" 8 | ], 9 | "extends": [ 10 | "prettier", 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/no-explicit-any": "off", 17 | "@typescript-eslint/no-unused-vars": "off", 18 | "prettier/prettier": [ 19 | "warn", { 20 | "printWidth": 100, 21 | "endOfLine": "lf" 22 | } 23 | ], 24 | "simple-import-sort/imports": "error", 25 | "simple-import-sort/exports": "error" 26 | } 27 | } -------------------------------------------------------------------------------- /src/database/redis/redis.database.ts: -------------------------------------------------------------------------------- 1 | import { InternalServeError } from "@shared/error/server.error"; 2 | 3 | import { appRedis } from "./redis.config"; 4 | 5 | class AppDatabase { 6 | redisHealthCheck = async () => { 7 | try { 8 | const redisHealth = await appRedis.ping(); 9 | console.log(`Redis Health: ${redisHealth}`); 10 | } catch (error: unknown) { 11 | throw new InternalServeError(error); 12 | } finally { 13 | await appRedis.quit(); 14 | } 15 | }; 16 | 17 | redisDisconnect = async () => { 18 | try { 19 | await appRedis.disconnect(); 20 | } catch (error: unknown) { 21 | throw new InternalServeError(error); 22 | } 23 | }; 24 | } 25 | 26 | export { AppDatabase }; 27 | -------------------------------------------------------------------------------- /src/app.route.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyRoute } from "@module/currency/currency.route"; 2 | import { HealthCheckRoute } from "@module/health/health.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 CurrencyRoute()]; 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/currency/currency.controller.ts: -------------------------------------------------------------------------------- 1 | import { getCurrencyException } from "@module/currency/currency.exception"; 2 | import { CurrencyService } from "@module/currency/currency.service"; 3 | import { SuccessOk } from "@shared/response/success/success.response"; 4 | import { Request, Response } from "express"; 5 | 6 | class CurrencyController { 7 | currencyService: CurrencyService; 8 | 9 | constructor() { 10 | this.currencyService = new CurrencyService(); 11 | } 12 | 13 | getCurrency = async (request: Request, response: Response) => { 14 | try { 15 | const resultGetCurrency = await this.currencyService.getCurrency(); 16 | SuccessOk(response, resultGetCurrency); 17 | } catch (error: unknown) { 18 | getCurrencyException(error); 19 | } 20 | }; 21 | } 22 | 23 | export { CurrencyController }; 24 | -------------------------------------------------------------------------------- /src/shared/error/client.error.ts: -------------------------------------------------------------------------------- 1 | import status from "http-status"; 2 | 3 | import { CustomError } from "./custom/custom.error"; 4 | 5 | export class NotFound extends CustomError { 6 | constructor(errorRawMessage?: unknown) { 7 | super({ 8 | errorCode: Number(status["NOT_FOUND"]), 9 | errorName: String(status[`${status.NOT_FOUND}_NAME`]), 10 | errorMessage: String(status[`${status.NOT_FOUND}_MESSAGE`]), 11 | errorRawMessage: errorRawMessage 12 | }); 13 | } 14 | } 15 | 16 | export class BadRequest extends CustomError { 17 | constructor(errorRawMessage?: unknown) { 18 | super({ 19 | errorCode: Number(status["BAD_REQUEST"]), 20 | errorName: String(status[`${status.BAD_REQUEST}_NAME`]), 21 | errorMessage: String(status[`${status.BAD_REQUEST}_MESSAGE`]), 22 | errorRawMessage: errorRawMessage 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app.terminator.ts: -------------------------------------------------------------------------------- 1 | import { AppDatabase } from "@database/redis/redis.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().redisDisconnect(); 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 | } -------------------------------------------------------------------------------- /express-ioredis.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "83d7c27e-af37-45bb-9aae-355ac454cd49", 4 | "name": "express-ioredis", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "6924403" 7 | }, 8 | "item": [ 9 | { 10 | "name": "01. 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": "02. api/v1/currency", 31 | "request": { 32 | "method": "GET", 33 | "header": [], 34 | "url": { 35 | "raw": "http://localhost:5000/api/v1/currency", 36 | "protocol": "http", 37 | "host": [ 38 | "localhost" 39 | ], 40 | "port": "5000", 41 | "path": [ 42 | "api", 43 | "v1", 44 | "currency" 45 | ] 46 | } 47 | }, 48 | "response": [] 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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.endOfLine": "lf", 24 | "prettier.singleQuote": false, 25 | "prettier.trailingComma": "none", 26 | 27 | "npm-intellisense.importES6": true, 28 | "npm-intellisense.importQuotes": "'", 29 | "npm-intellisense.importLinebreak": ";\r\n", 30 | "npm-intellisense.importDeclarationType": "const", 31 | 32 | "javascript.suggestionActions.enabled": false, 33 | 34 | "terminal.integrated.defaultProfile.windows": "GitBash", 35 | "terminal.integrated.profiles.windows": { 36 | "GitBash": { 37 | "source": "Git Bash", 38 | "path": [ 39 | "C:\\Program Files\\Git\\bin\\bash.exe" 40 | ], 41 | "icon": "terminal-bash" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { AppDatabase } from "@database/redis/redis.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 | await new AppDatabase().redisHealthCheck(); 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-ioredis", 3 | "version": "1.0.0", 4 | "description": "express + typescript + redis", 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 | "axios": "^1.6.5", 19 | "cors": "^2.8.5", 20 | "envalid": "^8.0.0", 21 | "express": "^4.18.2", 22 | "express-async-errors": "^3.1.1", 23 | "http-status": "^1.7.3", 24 | "http-terminator": "^3.2.0", 25 | "ioredis": "^5.3.2" 26 | }, 27 | "devDependencies": { 28 | "@types/cors": "^2.8.17", 29 | "@types/express": "^4.17.21", 30 | "@types/ioredis": "^5.0.0", 31 | "@types/module-alias": "^2.0.4", 32 | "@types/node": "^20.11.0", 33 | "@typescript-eslint/eslint-plugin": "^6.18.1", 34 | "@typescript-eslint/parser": "^6.18.1", 35 | "cross-env": "^7.0.3", 36 | "esbuild": "^0.19.11", 37 | "eslint": "^8.56.0", 38 | "eslint-config-prettier": "^9.1.0", 39 | "eslint-plugin-prettier": "^5.1.3", 40 | "eslint-plugin-simple-import-sort": "^10.0.0", 41 | "module-alias": "^2.2.3", 42 | "prettier": "^3.1.1", 43 | "rimraf": "^5.0.5", 44 | "tsx": "^4.7.0", 45 | "typescript": "^5.3.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /developer.docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | redis: 5 | image: redis:latest 6 | container_name: redis 7 | restart: always 8 | command: ["redis-server", "--bind", "redis", "--port", "6379"] 9 | volumes: 10 | - redis:/var/lib/redis 11 | - redis-config:/usr/local/etc/redis/redis.conf 12 | ports: 13 | - 6379:6379 14 | networks: 15 | - app-network 16 | 17 | redis-commander: 18 | depends_on: 19 | - redis 20 | image: rediscommander/redis-commander:latest 21 | container_name: redis-commander 22 | restart: always 23 | environment: 24 | - REDIS_HOSTS=local:redis:6379 25 | - HTTP_USER=root 26 | - HTTP_PASSWORD=qwerty 27 | ports: 28 | - 8081:8081 29 | networks: 30 | - app-network 31 | 32 | express-ioredis: 33 | depends_on: 34 | - redis 35 | build: 36 | context: . 37 | dockerfile: developer.dockerfile 38 | image: express-ioredis 39 | container_name: express-ioredis 40 | ports: 41 | - "5000:5000" 42 | environment: 43 | APP_PORT: 5000 44 | REDIS_PORT: 6379 45 | REDIS_HOST: redis 46 | volumes: 47 | - .:/home/node/app 48 | - node_modules:/home/node/app/node_modules 49 | command: > 50 | bash -c " 51 | npm run app:typecheck 52 | npm run app:lint 53 | npm run app:build 54 | npm run app:start:dev 55 | " 56 | restart: unless-stopped 57 | networks: 58 | - app-network 59 | 60 | networks: 61 | app-network: 62 | driver: bridge 63 | 64 | volumes: 65 | redis: 66 | redis-config: 67 | node_modules: 68 | -------------------------------------------------------------------------------- /src/module/currency/currency.service.ts: -------------------------------------------------------------------------------- 1 | import { appRedis } from "@database/redis/redis.config"; 2 | import { getCurrencyException } from "@module/currency/currency.exception"; 3 | import { ICurrencyService } from "@module/currency/currency.interface"; 4 | import axios from "axios"; 5 | 6 | class CurrencyService implements ICurrencyService { 7 | async getCurrency() { 8 | const FIXER_API_URL = "http://data.fixer.io/api/latest"; 9 | const FIXER_ACCESS_KEY = ""; // Fixer API Access Key 10 | const FIXER_SYMBOLS = "INR"; 11 | 12 | try { 13 | await appRedis.connect(); 14 | console.log("Connect to the Redis cache"); 15 | const cacheCurrency = await appRedis.get("cacheCurrency"); 16 | 17 | if (cacheCurrency) { 18 | // If data exists in the cache, return it 19 | console.log("Fetching data from Redis cache"); 20 | return JSON.parse(cacheCurrency); 21 | } else { 22 | console.log("Fetching data from Fixer URI"); 23 | const fixerURI = `${FIXER_API_URL}?access_key=${FIXER_ACCESS_KEY}&symbols=${FIXER_SYMBOLS}`; 24 | const fixerResponse = await axios.get(fixerURI); 25 | const fixerResponsSuccess = fixerResponse.data?.success; 26 | const fixerResponseDataINR = fixerResponsSuccess ? fixerResponse.data?.rates?.INR : ""; 27 | const getCurrencyResponse = { currency: { eur: 1, inr: fixerResponseDataINR } }; 28 | 29 | // Cache for 1 hour 30 | await appRedis.set("cacheCurrency", JSON.stringify(getCurrencyResponse), "EX", 3600); 31 | return getCurrencyResponse; 32 | } 33 | } catch (error: unknown) { 34 | getCurrencyException(error); 35 | } finally { 36 | await appRedis.quit(); 37 | console.log("Close connect to the Redis cache"); 38 | } 39 | } 40 | } 41 | 42 | export { CurrencyService }; 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-ioredis 2 | 3 | ``` 4 | 5 | Up application: 6 | docker-compose -f developer.docker-compose.yml up -d 7 | 8 | Down application: 9 | docker-compose -f developer.docker-compose.yml down -v --rmi all 10 | 11 | Steps to login in redis-commander: 12 | 13 | 1. Open redis-commander in the web browser by visiting http://localhost:8081. 14 | 2. To log in, fill in the following details 15 | > Username : root 16 | > Password : qwerty 17 | 18 | Q1. Why Redis is commonly paired with Express.js ? 19 | 20 | Redis is commonly paired with Express.js to enhance web application development. 21 | 22 | 1. Caching: Redis offers fast in-memory storage for caching, reducing database load and improving response times. 23 | 24 | 2. Session Management: Efficient handling of user sessions across multiple instances, ensuring authentication persistence during scaling. 25 | 26 | 3. Real-time Features: Utilize Redis' publish-subscribe messaging for building live chat, notifications, and real-time updates. 27 | 28 | 4. Rate Limiting & Throttling: Implement mechanisms to control access rates and prevent abuse through Redis. 29 | 30 | 5. Task Queue: Manage background processing and job execution asynchronously using Redis as a task queue. 31 | 32 | 6. Fast Data Access: Leverage Redis's sub-millisecond response times for quick data retrieval, enhancing performance. 33 | 34 | 7. Distributed Caching: Act as a centralized caching layer for consistent data access in multi-server or microservices architectures. 35 | 36 | 8. Flexible Data Structures: Support for various data structures enables modeling data for specific use cases. 37 | 38 | 9. Persistence: Configurable persistence to disk ensures durability for critical data while maintaining speed advantages. 39 | 40 | Reference: 41 | 42 | Foreign exchange rates and currency conversion JSON API 43 | https://fixer.io/ 44 | 45 | Caching Strategies in Node.js: Improving Performance and Efficiency 46 | https://levelup.gitconnected.com/caching-strategies-in-node-js-improving-performance-and-efficiency-6a7fd929e165 47 | 48 | Caching and Beyond: Harnessing Redis with Node.js 49 | https://medium.com/@hussainghazali/caching-and-beyond-harnessing-redis-with-node-js-b692b1508f41 50 | 51 | How To Implement Caching in Node.js Using Redis 52 | https://www.digitalocean.com/community/tutorials/how-to-implement-caching-in-node-js-using-redis 53 | 54 | ``` 55 | --------------------------------------------------------------------------------