Skip to content

Project Conventions

Based on story-desk-be patterns. This document serves as the reference for code style, structure, and practices for jadu-auth-be.

Technology Stack

Layer Technology
Runtime Node.js with TypeScript
Module CommonJS (module: commonjs)
Framework Express.js v5
Database MongoDB (native driver)
Validation Zod
Auth BetterAuth + JWT plugin

Project Structure

src/
├── index.ts                    # Entry point, Express app setup
├── lib/
│   ├── db.ts                   # Database connection (getDatabase, getClient, closeDatabase)
│   └── auth.ts                 # BetterAuth configuration
├── {feature}/                  # Feature modules (camelCase folders)
│   ├── {feature}.router.ts     # Route definitions
│   ├── {feature}.controller.ts # Request handlers
│   ├── {feature}.service.ts    # Business logic
│   ├── {feature}.validator.ts  # Zod validation schemas
│   ├── {feature}.middleware.ts # Feature-specific middleware
│   └── {feature}.helper.ts     # Feature utilities
├── models/                     # MongoDB collection models
│   └── {entity}.model.ts
├── shared/                     # Cross-cutting utilities
│   ├── sharedHelpers.ts
│   ├── sharedErrors.ts
│   ├── sharedConstants.ts
│   └── sharedTypes.ts
└── thirdPartyServices/         # External API integrations (if used)
    └── {service}.service.ts

File Naming Conventions

  • Case: camelCase for all files
  • Suffix indicates purpose:
  • .router.ts - Express route definitions
  • .controller.ts - Request handlers (thin layer)
  • .service.ts - Business logic
  • .validator.ts - Zod validation schemas
  • .model.ts - Database model/collection access
  • .middleware.ts - Express middleware
  • .helper.ts - Utility functions
  • .constants.ts - Static values
  • .types.ts - TypeScript types/interfaces

Examples: - auth.router.ts - users.model.ts - sharedHelpers.ts

Code Style

Class Pattern

Use classes with static methods for services, controllers, helpers, and models:

export default class AuthService {
  static async loginUser(email: string, password: string): Promise<TokenPair> {
    // Business logic here
  }

  static async getUserData(userId: string): Promise<User | null> {
    // ...
  }
}

Export Pattern

  • Default exports for classes
  • Named exports for types, interfaces, and enums
// users.model.ts
export enum AccountType {
  USER = 'USER',
  ADMIN = 'ADMIN',
}

export interface User {
  userId: string;
  email: string;
  accountType: AccountType;
}

export default class UsersModel {
  static async getUser(email: string): Promise<User | null> {
    // ...
  }
}

Import Pattern

import AuthService from './auth.service';
import SharedHelpers from '../shared/sharedHelpers';
import { AccountType, User } from '../models/users.model';

TypeScript Rules

  • Explicit return types required (ESLint enforced)
  • @typescript-eslint/no-explicit-any: off (allowed but discouraged)
  • Strict mode enabled

Formatting (Prettier)

{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "es5",
  "tabWidth": 2,
  "printWidth": 180,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

Architecture Patterns

Request Flow

Router -> Controller -> Validator -> Service -> Model -> MongoDB

Controller Pattern

Controllers are thin - validate input, call service, send response:

export default class AuthController {
  static login = async (req: Request, res: Response): Promise<void> => {
    const { emailId, password } = AuthValidators.validateLoginRequest(req);
    const { jwtToken, refreshToken } = await AuthService.loginUser(emailId, password);
    SharedHelpers.sendResponse(res, 200, 'User successfully logged in', true, { jwtToken, refreshToken });
  };
}

Router Pattern

Wrap handlers with SharedHelpers.handleIfError() for consistent error handling:

const authRouter = Router();

authRouter.post('/login', SharedHelpers.handleIfError(AuthController.login));
authRouter.post('/signup', SharedHelpers.handleIfError(AuthController.signup));

export default authRouter;

Validator Pattern

Use Zod schemas, return parsed data:

export default class AuthValidators {
  static validateLoginRequest = (req: Request) => {
    const schema = z.object({
      emailId: z.string().regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
      password: z.string(),
    });
    return schema.parse(req.body);
  };
}

Response Format

All API responses follow this structure:

{
  isSuccess: boolean;
  message: string;
  data: object;
}

Use SharedHelpers.sendResponse():

SharedHelpers.sendResponse(res, 200, 'Success message', true, { key: 'value' });
SharedHelpers.sendResponse(res, 400, 'Error message', false);

Error Handling

Custom errors extend SharedErrors.CustomError:

export default class SharedErrors {
  static CustomError = class extends Error {
    constructor(message: string, statusCode: number, data?: any) {
      super(message);
      this.name = this.constructor.name;
    }
  };

  static InvalidCredentialsError = class extends SharedErrors.CustomError { };
  static DocumentNotFoundError = class extends SharedErrors.CustomError { };
  static ForbiddenError = class extends SharedErrors.CustomError { };
  // etc.
}

Throw errors in services, handle in handleIfError wrapper.

Database Patterns

MongoDB Connection (lib/db.ts)

Use getDatabase() and getClient() for a single shared connection:

import { getDatabase, getClient } from '../lib/db';

// In models: get collection from db
const db = await getDatabase();
const collection = db.collection<MyDoc>(COLLECTION_NAME);

// For transactions: getClient() returns the same MongoClient
const client = await getClient();
const session = client.startSession();

Model Pattern

export default class UsersModel {
  static async getUser(emailId: string): Promise<User | null> {
    const userCollection: Collection<User> = await MongoDBClient.collection(SharedConstants.USERS_COLLECTION_NAME);
    return await userCollection.findOne({ email: emailId });
  }

  static async createNewUser(data: CreateUserInput): Promise<User> {
    const userCollection = await MongoDBClient.collection(SharedConstants.USERS_COLLECTION_NAME);
    const newUser: User = {
      userId: SharedHelpers.generateUUID(),
      // ...
    };
    await userCollection.insertOne(newUser);
    return newUser;
  }
}

Environment Variables

Naming Convention

  • Database: MONGODB_URI_{DB_NAME} (e.g., MONGODB_URI_JADU_AUTH)
  • Auth secrets: JWT_SECRET, BETTER_AUTH_SECRET
  • API keys: {SERVICE}_API_KEY (e.g., RESEND_API_KEY)
  • Feature flags: ENABLE_{FEATURE}

Required Variables

# Server
PORT=8084

# Database
MONGODB_URI_JADU_AUTH=mongodb+srv://...

# BetterAuth
BETTER_AUTH_SECRET=<32+ char secret>
BETTER_AUTH_URL=http://localhost:8084

# JWT (for custom signing if needed)
JWT_SECRET=<your secret>

Tooling

Git Hooks (Lefthook)

Pre-commit runs: 1. npm run lint - ESLint check 2. npm run build - TypeScript compilation

NPM Scripts

{
  "dev": "nodemon --watch src --exec ts-node src/index.ts",
  "start": "node dist/index.js",
  "build": "tsc",
  "lint": "eslint **/*.ts",
  "setup": "lefthook install"
}

Constants

Store in shared/sharedConstants.ts:

class SharedConstants {
  // Time: Secs x Min x Hours x Days
  static JWT_TOKEN_EXPIRY_IN_SECONDS = 60 * 60 * 24;        // 1 day
  static REFRESH_TOKEN_EXPIRY_IN_SECONDS = 60 * 60 * 24 * 10; // 10 days
  static OTP_EXPIRY_IN_SECONDS = 60 * 10;                    // 10 minutes

  // Database
  static readonly JADU_AUTH_DB_NAME = 'jadu_auth';
  static readonly USERS_COLLECTION_NAME = 'users';
  static readonly SESSIONS_COLLECTION_NAME = 'sessions';
}

export { SharedConstants };