Kanad
|

About

Projects

Blogs

Contact

|

©2025 / Kanad Shee/ Building Scalabale Products

©2025 / Kanad Shee

On This Page

Advanced Node.js Backend Starter

Monday, December 1, 2025

•

By Kanad Shee

Building a scalable and maintainable backend using Express, TypeScript, Winston, and more.

Advanced Node.js Backend Starter

Advanced Node.js Backend with TypeScript

Building a "Hello World" API in Node.js is easy. Building a backend that is robust, secure, scalable, and maintainable for a production environment is a different challenge entirely.

This guide provides a deep dive into creating a professional-grade backend using Node.js, Express, and TypeScript. We won't just throw code at you; we will explain the philosophy behind every decision, from folder structure to error handling strategies.

By the end of this guide, you will have a template that includes:

  • Type Safety: Full TypeScript integration for robust code.
  • Structured Logging: Multi-transport logging with Winston (Console, File, Database).
  • Centralized Error Handling: A consistent way to catch and process errors.
  • Standardized Responses: predictable API responses for frontend consumption.
  • Security: Helmet for headers, Rate Limiting for DDoS protection, and CORS.
  • Code Quality: ESLint, Prettier, and Husky for enforcing standards.
  • Scalability: A modular architecture that grows with your application.

Tools and Technologies

We have carefully selected a stack of libraries that are industry standards for modern Node.js development.

NameCategoryDescription & Why We Use It
Node.jsRuntimeThe JavaScript runtime built on Chrome's V8 engine. It's non-blocking and event-driven, making it perfect for I/O-heavy applications.
ExpressFrameworkThe most popular web framework for Node.js. It's unopinionated, meaning we have full control over the architecture.
TypeScriptLanguageA superset of JavaScript that adds static types. It catches errors at compile-time rather than runtime, which is crucial for large codebases.
WinstonLoggingA versatile logging library. Unlike console.log, it supports multiple "transports" (destinations) and log levels (info, error, warn).
HelmetSecurityA middleware that sets various HTTP headers to secure the app against common vulnerabilities like XSS and clickjacking.
Rate Limiter FlexibleSecurityProtects your API from brute-force attacks and DDoS by limiting the number of requests a user can make in a given timeframe.
MongooseDatabaseAn Object Data Modeling (ODM) library for MongoDB. It provides schema validation and an easy-to-use API for database interactions.
HuskyDevOpsA tool for using Git hooks. We use it to run linters and tests before a commit is allowed, ensuring bad code never enters the repo.
CommitlintDevOpsEnforces "Conventional Commits" (e.g., feat: add login). This keeps the git history clean and enables automated changelogs.
ESLintQualityA linter that analyzes code for potential errors and style violations.
PrettierFormattingAn opinionated code formatter. It ensures that all code looks the same, regardless of who wrote it.
NodemonDev ToolAutomatically restarts the server when file changes are detected, speeding up development.
Dotenv FlowConfigLoads environment variables from .env files. It supports multiple environments (dev, test, prod) better than standard dotenv.
Cross-EnvUtilityAllows you to set environment variables in scripts (like NODE_ENV) in a way that works across Windows, Linux, and macOS.

Project Structure: The Architecture

A good folder structure is the backbone of a maintainable project. We use a Layered Architecture (also known as Separation of Concerns).

server/
├── .husky/                 # Git hooks configuration
├── logs/                   # Local log files (ignored by git)
├── public/                 # Static assets (images, favicon)
├── src/
│   ├── config/             # Configuration layer
│   │   ├── config.ts       # Environment variables
│   │   └── rateLimiter.ts  # Rate limiter setup
│   ├── constant/           # Constants layer
│   │   ├── application.ts  # App-wide enums
│   │   └── responseMessage.ts # Standardized messages
│   ├── controller/         # Controller layer (Request/Response handling)
│   ├── middleware/         # Middleware layer (Interceptors)
│   ├── model/              # Data Access layer (Database Schemas)
│   ├── router/             # Routing layer (URL definitions)
│   ├── service/            # Service layer (Business Logic)
│   ├── types/              # TypeScript definitions
│   ├── util/               # Utilities (Helpers, Logger, Error objects)
│   ├── app.ts              # Express App setup
│   └── server.ts           # Entry point
├── .env.development        # Dev environment variables
├── .env.production         # Prod environment variables
├── package.json            # Dependencies and scripts
└── tsconfig.json           # TypeScript compiler config

Why this structure?

  • Config: Keeps configuration separate from code. If the DB URL changes, you change it in one place.
  • Controllers: Handle the HTTP request, parse parameters, call services, and send the response. They should contain no business logic.
  • Services: Contain the business logic. They interact with the database (Models) and return data to the controller. This makes logic reusable and testable.
  • Models: Define the data structure.
  • Utils: Helper functions used across the app.

Project Initialization & Dependencies

The package.json file defines our project identity and scripts.

Key Highlights:

  • type: "module": We are using ECMAScript Modules (ESM) (import/export) instead of CommonJS (require). This is the modern standard.
  • scripts:
    • dev: Uses tsx (TypeScript Execute) to run .ts files directly without compiling, which is much faster for development.
    • build: Compiles TS to JS in the dist folder for production.
    • start: Runs the compiled JS code. Never run ts-node or tsx in production as it consumes unnecessary resources.

package.json

{
  "name": "backend-production-setup",
  "version": "1.0.0",
  "description": "Production ready Node.js backend",
  "license": "ISC",
  "author": "Kanad Shee",
  "type": "module",
  "main": "dist/server.js",
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon --exec tsx src/server.ts",
    "start": "cross-env NODE_ENV=production node dist/server.js",
    "build": "npx tsc",
    "lint": "eslint",
    "lint:fix": "eslint --fix",
    "format:check": "prettier . --check",
    "format:fix": "prettier . --fix",
    "prepare": "husky"
  },
  "lint-staged": {
    "*.ts": ["npm run lint:fix", "npm run format:fix"]
  },
  "devDependencies": {
    "@commitlint/cli": "^20.2.0",
    "@commitlint/config-conventional": "^20.2.0",
    "@eslint/js": "^9.39.1",
    "@types/cookie-parser": "^1.4.10",
    "@types/cors": "^2.8.19",
    "@types/express": "^5.0.6",
    "@types/mongoose": "^5.11.97",
    "@types/node": "^24.10.1",
    "@types/source-map-support": "^0.5.10",
    "eslint": "^9.39.1",
    "eslint-config-prettier": "^10.1.8",
    "husky": "^9.1.7",
    "lint-staged": "^16.2.7",
    "nodemon": "^3.1.11",
    "prettier": "3.7.4",
    "ts-node": "^10.9.2",
    "tsx": "^4.21.0",
    "typescript": "^5.9.3",
    "typescript-eslint": "^8.48.1"
  },
  "dependencies": {
    "colorette": "^2.0.20",
    "cookie-parser": "^1.4.7",
    "cors": "^2.8.5",
    "cross-env": "^10.1.0",
    "dotenv-flow": "^4.1.0",
    "express": "^5.2.1",
    "helmet": "^8.1.0",
    "mongoose": "^9.0.1",
    "rate-limiter-flexible": "^9.0.0",
    "source-map-support": "^0.5.21",
    "ts-migrate-mongoose": "^4.2.0",
    "winston": "^3.18.3",
    "winston-mongodb": "^7.0.1"
  }
}

TypeScript Configuration

The tsconfig.json tells the compiler how to behave. We want strictness to prevent bugs.

Key Settings:

  • module: "nodenext": Aligns with Node.js's native ESM support.
  • sourceMap: true: Essential for production debugging. It allows stack traces to point to your .ts files even when running the .js code.
  • strict: true: Enables a suite of strict type checking options (like noImplicitAny). This forces you to write better code.
  • outDir: "./dist": Keeps your source code separate from build artifacts.

tsconfig.json

{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "module": "nodenext",
    "target": "esnext",
    "lib": ["esnext"],
    "types": ["node"],
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "forceConsistentCasingInFileNames": true,
    "strictPropertyInitialization": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "strict": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true
  }
}

Code Quality & Tooling

In a team environment, enforcing code style is critical.

ESLint (The Linter)

We use the new "Flat Config" system (eslint.config.mjs).

  • no-console: We set this to error. In production, console.log is synchronous and can block the event loop. It also doesn't provide log levels or timestamps. We use winston instead.
  • @typescript-eslint/no-explicit-any: Warns against using any, which defeats the purpose of TypeScript.

eslint.config.mjs

// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import { defineConfig } from 'eslint/config';
import eslintConfigPrettier from 'eslint-config-prettier';

export default defineConfig([
  { ignores: ['dist/**'] },
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  {
    files: ['**/*.ts'],
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      '@typescript-eslint/array-type': 'error',
      '@typescript-eslint/no-explicit-any': ['warn', { ignoreRestArgs: true }],
      'no-console': 'error',
      'no-useless-catch': 0,
      quotes: ['error', 'single', { allowTemplateLiterals: true }],
    },
    extends: [eslintConfigPrettier],
  },
  {
    files: ['*.cjs', '**/*.mjs'],
    extends: [tseslint.configs.disableTypeChecked],
    languageOptions: {
      globals: {
        module: 'readonly',
        require: 'readonly',
        __dirname: 'readonly',
        __filename: 'readonly',
      },
    },
    rules: { 'no-undef': 'off' },
  },
]);

Commitlint (The Gatekeeper)

This ensures your commit messages are descriptive.

  • Bad: fixed it
  • Good: fix: resolve null pointer exception in user service

commitlint.config.cjs

module.exports = {
  extends: ['@commitlint/cli', '@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',
        'fix',
        'docs',
        'style',
        'refractor',
        'perf',
        'test',
        'build',
        'ci',
        'chore',
        'revert',
      ],
    ],
    'subject-case': [2, 'always', 'sentence-case'],
  },
};

Nodemon (The Watcher)

Configured to ignore dist and node_modules to save CPU cycles.

nodemon.json

{
  "ext": ".ts",
  "ignore": ["dist", "node_modules"]
}

Git Hooks with Husky

Husky intercepts git commands.

.husky/pre-commit Runs lint-staged. This means if you try to commit code that has linting errors or isn't formatted, the commit will fail. This keeps the main branch clean.

npx lint-staged

.husky/commit-msg Runs commitlint to check your message format.

npx --no-install commitlint --edit "$1"

Configuration & Constants

Hardcoding values is a sin in software development. We use a centralized configuration pattern.

src/config/config.ts This file is the single source of truth for environment variables.

  • It validates that variables exist.
  • It exports them as a typed object.
import dotenvFlow from 'dotenv-flow';
dotenvFlow.config();

export default {
  ENV: process.env.ENV,
  PORT: process.env.PORT,
  SERVER_URL: process.env.SERVER_URL,
  DATABASE_URL: process.env.DATABASE_URL,
};

src/constant/application.ts Using Enums prevents "magic string" errors. If you mistype 'production' as 'prodution', the compiler won't catch it. If you mistype EApplicationEnvironment.PRODUCTION, it will.

export enum EApplicationEnvironment {
  PRODUCTION = 'production',
  DEVELOPMENT = 'development',
}

src/constant/responseMessage.ts Centralizing messages allows for easy localization (i18n) in the future and ensures consistency.

export default {
  SUCCESS: `Operation has been successfull`,
  SOMETHING_WENT_WRONG: `Something went wrong`,
  NOT_FOUND: (entity: string) => `${entity} not found.`,
  TOO_MANY_REQUEST: `Too many requests. Try again after some time.`,
};

Types & Utilities

TypeScript shines when you define your data structures clearly.

src/types/types.ts We define a strict contract for our API responses. Every single response from our server will follow this structure. This makes life incredibly easy for frontend developers.

export type THttpResponse = {
  success: boolean;
  statusCode: number;
  request: {
    ip?: string | null;
    method: string;
    url: string;
  };
  message: string;
  data: unknown;
};

export type THttpError = {
  success: boolean;
  statusCode: number;
  request: {
    ip?: string | null;
    method: string;
    url: string;
  };
  message: string;
  data: unknown;
  trace?: object | null;
};

The Logger (Winston)

This is one of the most critical parts of a production app.

  • Console Transport: Used in development. We use colorette to make it readable.
  • File Transport: Used in production. Logs are written to files (e.g., production.log).
  • MongoDB Transport: Logs are saved to the database. This allows you to build an admin dashboard to view/search logs easily.
  • Source Maps: We use source-map-support so that when an error occurs, the stack trace points to src/controller/user.ts:25 instead of dist/controller/user.js:15.

src/util/logger.ts

import { createLogger, format, transports } from 'winston';
import 'winston-mongodb';
import util from 'util';
import config from '../config/config.js';
import { EApplicationEnvironment } from '../constant/application.js';
import path from 'path';
import * as sourceMapSupport from 'source-map-support';
import { blue, green, magenta, red, yellow } from 'colorette';

sourceMapSupport.install();

const colorizeLevel = (level: string) => {
  switch (level) {
    case 'ERROR':
      return red(level);
    case 'INFO':
      return blue(level);
    case 'WARN':
      return yellow(level);
    default:
      return level;
  }
};

const consoleLogFormat = format.printf((info) => {
  const { level, message, timestamp, meta = {} } = info;
  const customLevel = colorizeLevel(String(level).toUpperCase());
  const customTimeStamp = green(String(timestamp));
  const customMessage = String(message);
  const customMeta = util.inspect(meta, {
    showHidden: false,
    depth: null,
    colors: true,
  });
  return `${customLevel} [${customTimeStamp}] ${customMessage}\n${magenta('META')} ${customMeta}\n`;
});

const fileLogFormat = format.printf((info) => {
  const { level, message, timestamp, meta = {} } = info;
  const metaObj = meta as Record<string, unknown>;
  const logMeta: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(metaObj)) {
    if (value instanceof Error) {
      logMeta[key] = {
        name: value.name,
        message: value.message,
        trace: value.stack || '',
      };
    } else {
      logMeta[key] = value;
    }
  }
  return JSON.stringify(
    { level: level.toUpperCase(), message, timestamp, meta: logMeta },
    null,
    4,
  );
});

const consoleTransport = () => {
  if (config.ENV === EApplicationEnvironment.DEVELOPMENT) {
    return [
      new transports.Console({
        level: 'info',
        format: format.combine(format.timestamp(), consoleLogFormat),
      }),
    ];
  }
  return [];
};

const fileTransport = () => {
  return [
    new transports.File({
      filename: path.join(
        import.meta.dirname,
        '../',
        '../',
        'logs',
        `${config.ENV}.log`,
      ),
      level: 'info',
      format: format.combine(format.timestamp(), fileLogFormat),
    }),
  ];
};

const dbTransport = () => {
  return [
    new transports.MongoDB({
      level: 'info',
      db: config.DATABASE_URL as string,
      metaKey: 'meta',
      expireAfterSeconds: 3600 * 24 * 30,
      collection: 'application-logs',
    }),
  ];
};

export default createLogger({
  defaultMeta: { meta: {} },
  transports: [...consoleTransport(), ...fileTransport(), ...dbTransport()],
});

Error Object Standardization

When an error occurs, we need to format it before sending it to the client.

  • Security: In production, we never send the stack trace (trace) or the server IP to the client. This information could be used by attackers.
  • Context: We include the request method and URL so we know exactly where the error happened.

src/util/errorObject.ts

import type { Request } from 'express';
import type { THttpError } from '../types/types.js';
import responseMessage from '../constant/responseMessage.js';
import config from '../config/config.js';
import { EApplicationEnvironment } from '../constant/application.js';
import logger from './logger.js';

export const errorObjet = (
  err: unknown,
  req: Request,
  errorStatusCode: number = 500,
) => {
  const errorObj: THttpError = {
    success: false,
    statusCode: errorStatusCode,
    request: { ip: req.ip || null, method: req.method, url: req.originalUrl },
    message:
      err instanceof Error
        ? err.message || responseMessage.SOMETHING_WENT_WRONG
        : responseMessage.SOMETHING_WENT_WRONG,
    data: null,
    trace: err instanceof Error ? { error: err.stack } : null,
  };
  logger.error(`CONTROLLER_ERROR`, { meta: errorObj });
  if (config.ENV === EApplicationEnvironment.PRODUCTION) {
    delete errorObj.request.ip;
    delete errorObj.trace; // Ensure trace is removed in production
  }
  return errorObj;
};

src/util/httpError.ts A wrapper to simplify calling the error handler from controllers.

import type { NextFunction, Request } from 'express';
import { errorObjet } from './errorObject.js';

export const httpError = (
  next: NextFunction,
  err: unknown,
  req: Request,
  errorStatusCode: number = 500,
): void => {
  const errorObj = errorObjet(err, req, errorStatusCode);
  return next(errorObj);
};

src/util/httpResponse.ts A wrapper to simplify sending success responses. It also logs every successful response, which is great for analytics.

import type { Request, Response } from 'express';
import type { THttpResponse } from '../types/types.js';
import config from '../config/config.js';
import { EApplicationEnvironment } from '../constant/application.js';
import logger from './logger.js';

export const httpResponse = (
  req: Request,
  res: Response,
  responseStatusCode: number,
  responseMessage: string,
  data: unknown = null,
): void => {
  const response: THttpResponse = {
    success: true,
    statusCode: responseStatusCode,
    message: responseMessage,
    request: { ip: req.ip || null, method: req.method, url: req.originalUrl },
    data,
  };
  logger.info(`CONTROLLER_RESPONSE`, { meta: response });
  if (config.ENV === EApplicationEnvironment.PRODUCTION) {
    delete response.request.ip;
  }
  res.status(responseStatusCode).json(response);
};

src/util/quicker.ts A utility to get system health. Useful for monitoring.

import os from 'node:os';
import config from '../config/config.js';

export default {
  getSystemHealth: () => {
    return {
      cpuUsage: os.loadavg(),
      totalMemory: `${(os.totalmem() / 1024 / 1024).toFixed(2)} MB`,
      freeMemory: `${(os.freemem() / 1024 / 1024).toFixed(2)} MB`,
    };
  },
  getApplicationHealth: () => {
    return {
      environment: config.ENV,
      uptime: `${process.uptime().toFixed(2)} Second`,
      memoryUsage: {
        heapTotal: `${(process.memoryUsage().heapTotal / 1024 / 1024).toFixed(2)} MB`,
        heapUsed: `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`,
      },
    };
  },
};

Services & Middleware

Database Service

We encapsulate the DB connection logic. This makes it easier to swap databases or change connection logic later.

src/service/databaseService.ts

import mongoose from 'mongoose';
import config from '../config/config.js';

export default {
  connectDB: async () => {
    try {
      await mongoose.connect(config.DATABASE_URL as string);
      return mongoose.connection;
    } catch (error) {
      throw error;
    }
  },
};

Rate Limiter

DDoS protection is mandatory for production APIs. We use rate-limiter-flexible with MongoDB.

  • Why MongoDB? If you run multiple instances of your server (e.g., using PM2 or Kubernetes), an in-memory rate limiter would only work per-instance. A database-backed limiter works across all instances.

src/config/rateLimiter.ts

import type { Connection } from 'mongoose';
import { RateLimiterMongo } from 'rate-limiter-flexible';

export let rateLimiterMongo: null | RateLimiterMongo = null;
const DURATION = 60;
const POINTS = 10; // 10 requests per 60 seconds

export const initRateLimiter = (mongooseConnection: Connection) => {
  rateLimiterMongo = new RateLimiterMongo({
    storeClient: mongooseConnection,
    points: POINTS,
    duration: DURATION,
  });
};

src/middleware/rateLimit.ts The middleware that checks the limit.

  • Dev Mode: We skip rate limiting in development so you don't block yourself while testing.
import type { NextFunction, Request, Response } from 'express';
import config from '../config/config.js';
import { EApplicationEnvironment } from '../constant/application.js';
import { rateLimiterMongo } from '../config/rateLimiter.js';
import { httpError } from '../util/httpError.js';
import responseMessage from '../constant/responseMessage.js';

export const rateLimiter = (req: Request, _: Response, next: NextFunction) => {
  if (config.ENV === EApplicationEnvironment.DEVELOPMENT) {
    return next();
  }
  if (rateLimiterMongo) {
    rateLimiterMongo
      .consume(req.ip as string, 1)
      .then(() => {
        next();
      })
      .catch(() => {
        httpError(next, new Error(responseMessage.TOO_MANY_REQUEST), req, 429);
      });
  }
};

Global Error Handler

This is the "catch-all" for your application. If any part of your code throws an error and passes it to next(err), it ends up here.

  • It ensures the client always gets a JSON response, never a hanging request or an HTML error page.

src/middleware/globalErrorHandler.ts

import type { NextFunction, Request, Response } from 'express';
import type { THttpError } from '../types/types.js';

export const globalErrorHandler = (
  err: THttpError,
  _req: Request,
  res: Response,
  __: NextFunction,
) => {
  res.status(err.statusCode).json(err);
};

Controllers & Routes

API Controller

Simple controllers to test the system.

  • self: A basic connectivity test.
  • health: Returns the system health metrics we defined in quicker.ts.

src/controller/apiController.ts

import type { NextFunction, Request, Response } from 'express';
import { httpResponse } from '../util/httpResponse.js';
import responseMessage from '../constant/responseMessage.js';
import { httpError } from '../util/httpError.js';
import quicker from '../util/quicker.js';

export const self = (req: Request, res: Response, next: NextFunction) => {
  try {
    httpResponse(req, res, 200, responseMessage.SUCCESS);
  } catch (error) {
    httpError(next, error, req, 500);
  }
};

export const health = (req: Request, res: Response, next: NextFunction) => {
  try {
    const healthData = {
      application: quicker.getApplicationHealth(),
      system: quicker.getSystemHealth(),
      timestamp: Date.now(),
    };
    httpResponse(req, res, 200, responseMessage.SUCCESS, healthData);
  } catch (error) {
    httpError(next, error, req, 500);
  }
};

API Router

We group our routes here.

  • We apply the rateLimiter middleware to this router. This means all routes defined here are protected.

src/router/apiRouter.ts

import { Router } from 'express';
import { health, self } from '../controller/apiController.js';
import { rateLimiter } from '../middleware/rateLimit.js';

const router = Router();
router.use(rateLimiter);
router.route('/self').get(self);
router.route('/health').get(health);

export default router;

Application Entry Points

App Setup (app.ts)

This file configures the Express application but does not start the server. This separation is useful for testing (you can import app in tests without starting the server).

Key Middleware:

  • helmet(): Adds security headers.
  • cors(): Configures Cross-Origin Resource Sharing. We restrict it to specific domains in production.
  • cookieParser(): Parses cookies.
  • express.json(): Parses incoming JSON bodies.
  • 404 Handler: If a request matches no routes, we manually throw a 404 error.

src/app.ts

import cookieParser from 'cookie-parser';
import cors from 'cors';
import express, {
  type Application,
  type NextFunction,
  type Request,
  type Response,
} from 'express';
import helmet from 'helmet';
import path from 'node:path';
import responseMessage from './constant/responseMessage.js';
import { globalErrorHandler } from './middleware/globalErrorHandler.js';
import router from './router/apiRouter.js';
import { httpError } from './util/httpError.js';

const app: Application = express();

app.use(helmet());
app.use(
  cors({
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    origin: ['https://kanaddev.me'], // Replace with your frontend domain
    credentials: true,
  }),
);
app.use(cookieParser());
app.use(express.json());
app.use(express.static(path.join(import.meta.dirname, '../', 'public')));

app.use('/api/v1', router);

// 404 Handler
app.use((req: Request, _: Response, next: NextFunction) => {
  try {
    throw new Error(responseMessage.NOT_FOUND('Route'));
  } catch (error) {
    httpError(next, error, req, 404);
  }
});

// Global Error Handler
app.use(globalErrorHandler);

export default app;

Server Entry Point (server.ts)

This is the file that actually runs.

Startup Flow:

  1. Start listening on the port.
  2. Connect to the Database.
  3. Initialize the Rate Limiter (passing the DB connection).
  4. Log that the application has started.

Error Handling: If the DB connection fails, we catch the error, log it, and exit the process. We don't want the server running in a broken state.

src/server.ts

import app from './app.js';
import config from './config/config.js';
import { initRateLimiter } from './config/rateLimiter.js';
import databaseService from './service/databaseService.js';
import logger from './util/logger.js';

const server = app.listen(config.PORT);

void (async () => {
  try {
    const connection = await databaseService.connectDB();
    logger.info('DATABASE CONNECTION', {
      meta: { CONNECTION_NAME: connection.name },
    });

    initRateLimiter(connection);
    logger.info('RATE LIMITER INITIATED');

    logger.info(`APPLICATION_STARTED`, {
      meta: { PORT: config.PORT, SERVER_URL: config.SERVER_URL },
    });
  } catch (error) {
    logger.error(`APPLICATION ERROR`, { meta: error });
    server.close((err) => {
      if (err) {
        logger.error(`APPLICATION ERROR`, { meta: error });
      }
      process.exit(1);
    });
  }
})();

Running the Application

Now that the code is in place, here is how you run it.

Install Dependencies

npm install

Set up Environment Variables

Create a .env file in the root directory.

ENV=development
PORT=3000
SERVER_URL=http://localhost:3000
DATABASE_URL=mongodb://localhost:27017/your-db-name

Run in Development Mode

This will start the server with nodemon and tsx. Any change you make to a .ts file will automatically restart the server.

npm run dev

Build for Production

This compiles your TypeScript code into JavaScript in the dist folder.

npm run build

Start Production Server

This runs the compiled JavaScript code.

npm start

Conclusion

You now have a backend that is far superior to a basic Express setup. It is typed, logged, secured, and structured for growth.

  • Scalability: The folder structure allows you to add hundreds of routes without chaos.
  • Reliability: The error handling and strict TypeScript config prevent common crashes.
  • Maintainability: The code style tools ensure the codebase remains clean over time.

Feel free to clone this repository and use it as a starter template for your next big project!


Clone This Repository:

git clone https://github.com/KanadShee-18/Node-TS-Production-Backend