DEV Community

Rutvik Makvana
Rutvik Makvana

Posted on

Production level Nodejs Starter template

================================================================================
FILE: tsconfig.json
================================================================================
{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",

    "module": "nodenext",
    "target": "esnext",

    "types": ["node"],
    "sourceMap": false,
    "declaration": false,
    "declarationMap": false,

    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "strict": true,
    "verbatimModuleSyntax": false,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

================================================================================
FILE: package.json
================================================================================
{
  "name": "starter-template",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "dev": "concurrently --kill-others \"npm run watch\" \"nodemon dist/index.js\""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "dependencies": {
    "@prisma/client": "^6.19.3",
    "bcryptjs": "^3.0.3",
    "concurrently": "^9.2.1",
    "dotenv": "^17.4.2",
    "express": "^5.2.1",
    "jsonwebtoken": "^9.0.3",
    "multer": "^2.1.1",
    "passport": "^0.7.0",
    "passport-jwt": "^4.0.1",
    "zod": "^4.3.6"
  },
  "devDependencies": {
    "@types/bcrypt": "^6.0.0",
    "@types/express": "^5.0.6",
    "@types/jsonwebtoken": "^9.0.10",
    "@types/multer": "^2.1.0",
    "@types/passport": "^1.0.17",
    "@types/passport-jwt": "^4.0.1",
    "nodemon": "^3.1.14",
    "ts-node": "^10.9.2",
    "typescript": "^6.0.3"
  }
}

================================================================================
FILE: .env.example
================================================================================
PORT =

================================================================================
FILE: .gitignore
================================================================================
node_modules

/dist
.env
/src/generated/prisma

README.md

================================================================================
FILE: prisma/schema.prisma
================================================================================
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id           String   @id @default(uuid())
  email        String   @unique
  password     String
  profileImage String?  @map("profile_image")
  createdAt    DateTime @default(now()) @map("created_at")
  updatedAt    DateTime @updatedAt @map("updated_at")

  refreshTokens RefreshToken[]
  galleryImages GalleryImage[]

  @@map("users")
}

model RefreshToken {
  id        String   @id @default(uuid())
  token     String
  userId    String
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("refresh_tokens")
}

model GalleryImage {
  id       String @id @default(uuid())
  imageUrl String @map("image_url")
  userId   String

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("gallery_images")
}

================================================================================
FILE: src/app.ts
================================================================================
import express, { Express, Request, Response } from "express";
import errorMiddleware from "./shared/middlewares/error.middleware";
import path from "path";
import mainRouter from "./routes";

const app: Express = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/", (req, res) => {
  res.send("Welcome to the Express server!");
});

app.get("/health", (_req: Request, res: Response) => {
  return res.json({ status: "ok", timestamp: new Date().toISOString() });
});

app.use(mainRouter);

app.use("/public", express.static(path.join(__dirname, "../public")));
app.use(errorMiddleware);

export default app;

================================================================================
FILE: src/index.ts
================================================================================
import { serverConfig } from "./shared/config";
import "./shared/config/jwtPassport.config";
import app from "./app";

const PORT = serverConfig.port;

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

process.on("uncaughtException", (err) => {
  console.error("UNCAUGHT EXCEPTION:", err);
  process.exit(1);
});

process.on("unhandledRejection", (err) => {
  console.error("UNHANDLED REJECTION:", err);
  process.exit(1);
});

================================================================================
FILE: src/types/express.d.ts
================================================================================
// src/types/express.d.ts
import { User as PrismaUser } from "@prisma/client";

declare global {
  namespace Express {
    interface User extends PrismaUser {}

    interface Request {
      validated: {
        body: unknown;
        query: unknown;
        params: unknown;
      };

      user?: User;

      file?: Multer.File;
      files?: Multer.File[];
    }
  }
}

export {};

================================================================================
FILE: src/shared/config/index.ts
================================================================================
import "dotenv/config";

type ServerConfig = {
  port: number;
};

export const serverConfig: ServerConfig = {
  port: Number(process.env.PORT) || 3000,
};

================================================================================
FILE: src/shared/config/jwtPassport.config.ts
================================================================================
import passport from "passport";
import {
  ExtractJwt,
  Strategy,
  StrategyOptions,
  VerifiedCallback,
} from "passport-jwt";

import prisma from "../libs/prismaClient";

interface JwtPayload {
  userId: string;
  email: string;
  iat?: number;
  exp?: number;
}

const options: StrategyOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET!,
};

passport.use(
  "jwt",
  new Strategy(
    options,
    async (payload: JwtPayload, done: VerifiedCallback): Promise<void> => {
      try {
        if (!payload?.userId) {
          return done(null, false);
        }

        const user = await prisma.user.findUnique({
          where: {
            id: payload.userId,
          },
        });

        if (!user) {
          return done(null, false);
        }

        return done(null, user);
      } catch (error) {
        return done(error as Error, false);
      }
    },
  ),
);

export default passport;

================================================================================
FILE: src/shared/errors/appError.ts
================================================================================
export class AppError extends Error {
  public statusCode: number;
  public isOperational: boolean;

  constructor(message: string, statusCode = 500) {
    super(message);

    this.statusCode = statusCode;
    this.isOperational = true;

    Error.captureStackTrace(this, this.constructor);
  }
}

================================================================================
FILE: src/shared/libs/prismaClient.ts
================================================================================
import { PrismaClient } from "@prisma/client";

const globalForPrisma = global as unknown as {
  prisma: PrismaClient;
};

const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log:
      process.env.NODE_ENV === "development"
        ? ["query", "error", "warn"]
        : ["error"],
  });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

// Handle graceful shutdown
process.on("SIGINT", async () => {
  await prisma.$disconnect();
  process.exit(0);
});

process.on("SIGTERM", async () => {
  await prisma.$disconnect();
  process.exit(0);
});

export default prisma;

================================================================================
FILE: src/shared/middlewares/auth.middleware.ts
================================================================================
import { Request, Response, NextFunction } from "express";
import passport from "passport";
import { User } from "@prisma/client";

const authentication = (
  req: Request,
  res: Response,
  next: NextFunction,
): void => {
  passport.authenticate(
    "jwt",
    { session: false },
    (err: Error | null, user: User | false) => {
      if (err) {
        return next(err);
      }

      if (!user) {
        res.status(401).json({
          success: false,
          message: "Unauthorized",
        });

        return;
      }

      (req as Request).user = user;

      next();
    },
  )(req, res, next);
};

export default authentication;

================================================================================
FILE: src/shared/middlewares/error.middleware.ts
================================================================================
import { Request, Response, NextFunction } from "express";

const errorMiddleware = (
  err: any,
  _req: Request,
  res: Response,
  _next: NextFunction,
) => {
  let statusCode = err.statusCode || 500;
  let message = err.message || "Something went wrong";

  // Prisma errors handling
  if (err.code === "P2002") {
    statusCode = 400;
    message = "Duplicate field value";
  }

  // Development vs Production
  if (process.env.NODE_ENV === "development") {
    return res.status(statusCode).json({
      success: false,
      message,
      stack: err.stack,
      error: err,
    });
  }

  // Production response
  return res.status(statusCode).json({
    success: false,
    message,
  });
};

export default errorMiddleware;

================================================================================
FILE: src/shared/middlewares/upload.middleware.ts
================================================================================
import fs from "fs";
import path from "path";
import multer from "multer";
import { Request } from "express";

const storage = (destination: string) =>
  multer.diskStorage({
    destination: (req: Request, file: Express.Multer.File, cb) => {
      fs.mkdirSync(destination, { recursive: true });

      cb(null, destination);
    },

    filename: (req: Request, file: Express.Multer.File, cb) => {
      const ext = path.extname(file.originalname);

      const fileName = `${file.fieldname}-${Date.now()}${ext}`;

      cb(null, fileName);
    },
  });

export const uploadSingle = (fieldName: string, destination: string) => {
  return multer({
    storage: storage(destination),

    limits: {
      fileSize: 5 * 1024 * 1024, // 5MB
    },
  }).single(fieldName);
};

export const uploadMultiple = (
  fieldName: string,
  destination: string,
  maxCount = 5,
) => {
  return multer({
    storage: storage(destination),

    limits: {
      fileSize: 5 * 1024 * 1024, // 5MB
    },
  }).array(fieldName, maxCount);
};

================================================================================
FILE: src/shared/utils/catchAsync.ts
================================================================================
import { Request, Response, NextFunction } from "express";

export const catchAsync =
  (fn: any) => (req: Request, res: Response, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };

================================================================================
FILE: src/modules/auth/auth.service.ts
================================================================================
import { AppError } from "../../shared/errors/appError";
import prisma from "../../shared/libs/prismaClient";
import bcrypt from "bcryptjs";
import jwt, { SignOptions } from "jsonwebtoken";
import { StringValue } from "ms";

class authService {
  private readonly jwtSecret: string;
  private readonly jwtRefreshSecret: string;
  private readonly jwtExpiresIn: string;
  private readonly jwtRefreshExpiresIn: string;
  private readonly bcryptRounds: number;

  constructor() {
    this.jwtSecret = process.env.JWT_SECRET!;
    this.jwtRefreshSecret = process.env.JWT_REFRESH_SECRET!;
    this.jwtExpiresIn = process.env.JWT_EXPIRES_IN! || "15m";
    this.jwtRefreshExpiresIn = process.env.JWT_REFRESH_EXPIRES_IN! || "7d";
    this.bcryptRounds = Number(process.env.BCRYPT_ROUNDS! || 10);
  }

  /**
   * @description: Registration
   * @param data
   * @returns
   */
  async register(data: any) {
    const existingUser = await prisma.user.findUnique({
      where: { email: data.email },
    });

    if (existingUser) {
      throw new AppError("User already exists", 409);
    }

    const hashedPassword = await bcrypt.hash(data.password, this.bcryptRounds);

    const user = await prisma.user.create({
      data: {
        email: data.email,
        password: hashedPassword,
      },
    });

    return this.generateTokens(user.id, user.email);
  }

  /**
   * @description: Generate Access and Refresh Tokens
   * @param userId
   * @param email
   * @returns
   */
  async generateTokens(userId: string, email: string) {
    const payload = { userId, email };

    const accessTokenOptions: SignOptions = {
      expiresIn: this.jwtExpiresIn as StringValue,
    };

    const accessToken = jwt.sign(payload, this.jwtSecret, accessTokenOptions);

    const refreshTokenOptions: SignOptions = {
      expiresIn: this.jwtRefreshExpiresIn as StringValue,
    };

    const refreshToken = jwt.sign(
      payload,
      this.jwtRefreshSecret,
      refreshTokenOptions,
    );

    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 7); // Set expiration to 7 days

    await prisma.refreshToken.create({
      data: {
        token: refreshToken,
        userId,
        // expiresAt,
      },
    });

    return { accessToken, refreshToken };
  }
}

export default new authService();

================================================================================
FILE: src/modules/auth/auth.controller.ts
================================================================================
import authService from "./auth.service";
import { Request, Response } from "express";

class authController {
  /**
   * @description: Registration
   * @param req
   * @param res
   * @returns
   */
  async register(req: Request, res: Response) {
    const data = await authService.register(req.body);
    return res.send(data);
  }
}

export default new authController();

================================================================================
FILE: src/modules/auth/auth.routes.ts
================================================================================
import authController from "./auth.controller";
import { Router } from "express";
import { catchAsync } from "../../shared/utils/catchAsync";

const routes = Router();

routes.post("/register", catchAsync(authController.register));

export default routes;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)