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:
camelCasefor 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 };