π§ Backend Engineering Guidelines
This document defines the standards and conventions for all backend development in our company.
It ensures consistency, scalability, readability, and quality across all backend projects.
- Principles & Objectives
- Project Structure
- Naming Conventions
- Code Style & Documentation
- Testing Guidelines
- Security & Validation
- Git Commit & PR Standards
- Branching Strategy
- API & GraphQL Standards
- Error Handling
- Database Best Practices
- Performance Optimization
- Code Review Checklist
- Deployment Standards
- Logging & Monitoring
- Recommended Tools
- Onboarding Checklist
- Troubleshooting Guide
- Maintenance Rules
- Quick Reference
- Predictability: Code must be predictable, readable, and self-documented
- Consistency: APIs must be consistent, validated, and secure
- Quality: Code reviews should improve quality, not just check syntax
- Clarity: Favor clarity over cleverness
- Traceability: Every change should be traceable and testable
- Performance: Optimize for scalability without premature optimization
- Security: Security is not optional, it's a requirement
src/
βββ modules/ # Feature modules
β βββ user/
β β βββ user.module.ts # Module definition
β β βββ user.service.ts # Business logic
β β βββ user.resolver.ts # GraphQL resolver
β β βββ user.controller.ts # REST controller (optional)
β β βββ user.schema.ts # Mongoose schema
β β βββ dto/ # Data Transfer Objects
β β β βββ create-user.input.ts
β β β βββ update-user.input.ts
β β β βββ user.output.ts
β β βββ entities/ # Database entities
β β β βββ user.entity.ts
β β βββ tests/ # Unit & integration tests
β β βββ user.service.spec.ts
β β βββ user.resolver.spec.ts
β βββ auth/
β βββ order/
β βββ ...
βββ common/ # Shared utilities
β βββ decorators/ # Custom decorators (@CurrentUser, @Roles)
β βββ guards/ # Auth guards, role guards
β βββ interceptors/ # Logging, transform interceptors
β βββ filters/ # Exception filters
β βββ pipes/ # Validation pipes
β βββ utils/ # Helper functions
βββ config/ # Configuration files
β βββ app.config.ts
β βββ database.config.ts
β βββ validation.schema.ts
βββ graphql/ # GraphQL schema files
β βββ schema.graphql
βββ migrations/ # Database migrations
βββ main.ts # Application entry point
βββ app.module.ts # Root module
modules/: Self-contained feature modules following domain-driven design
common/: Shared code used across multiple modules
config/: Centralized configuration with environment validation
entities/: Database schema definitions
dto/: Input validation and output transformation objects
| Type |
Convention |
Example |
| Variables |
camelCase |
userProfile, totalCount |
| Functions / Methods |
verb + noun |
getUserById(), createOrder() |
| Classes |
PascalCase |
UserService, AuthGuard |
| Constants |
UPPER_SNAKE_CASE |
MAX_RETRY_COUNT, DEFAULT_PAGE_SIZE |
| Enums |
PascalCase |
UserRole, OrderStatus |
| Interfaces |
I prefix |
IUser, IAuthPayload |
| Types |
T prefix |
TUserRole, TApiResponse |
| Files |
kebab-case |
user.service.ts, auth.guard.ts |
| MongoDB Collections |
plural lowercase |
users, orders, products |
| DTOs / Inputs |
suffix-based |
CreateUserInput, UpdateOrderInput |
| Output Types |
suffix-based |
UserOutput, PaginatedResponse |
| GraphQL Queries |
descriptive |
getUsers, getUserById, searchOrders |
| GraphQL Mutations |
action verb |
createUser, updateUser, deleteUser |
| Environment Variables |
UPPER_SNAKE_CASE |
DATABASE_URL, JWT_SECRET |
- Use ESLint + Prettier to enforce consistency
- Max line length: 100 characters
- Indentation: 2 spaces
- Use single quotes for strings
- Always use TypeScript strict mode
- Avoid
any type - use proper types or unknown
- Document all public APIs with TSDoc
- Use async/await instead of raw promises
/**
* UserService
* Handles user-related business logic and database operations.
* @remarks Uses Mongoose for MongoDB interactions
*/
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
private readonly logger: LoggerService,
) {}
/**
* Create a new user in the system
* @param createUserInput - Data for new user
* @returns The created user document
* @throws {ConflictException} When email already exists
* @example
* const user = await userService.createUser({
* email: 'user@example.com',
* name: 'John Doe'
* });
*/
async createUser(createUserInput: CreateUserInput): Promise<User> {
this.logger.log('Creating new user', UserService.name);
const existingUser = await this.userModel.findOne({
email: createUserInput.email
});
if (existingUser) {
throw new ConflictException('Email already exists');
}
const newUser = new this.userModel(createUserInput);
return newUser.save();
}
/**
* Retrieve user by ID
* @param id - User ID
* @returns User document
* @throws {NotFoundException} When user not found
*/
async findById(id: string): Promise<User> {
const user = await this.userModel.findById(id).exec();
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
}
/**
* UserResolver
* GraphQL resolver for user-related queries and mutations
*/
@Resolver(() => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
/**
* Query to get all users with pagination
*/
@Query(() => PaginatedUserResponse)
async users(
@Args('page', { type: () => Int, defaultValue: 1 }) page: number,
@Args('limit', { type: () => Int, defaultValue: 10 }) limit: number,
): Promise<PaginatedUserResponse> {
return this.userService.findAll({ page, limit });
}
/**
* Mutation to create a new user
*/
@Mutation(() => User)
async createUser(
@Args('input') createUserInput: CreateUserInput,
): Promise<User> {
return this.userService.createUser(createUserInput);
}
}
/**
* @fileoverview Service for managing user authentication
* @author Backend Team
* @module auth
*/
- Unit Tests: Services, utils, guards, pipes, interceptors
- Integration Tests: Resolvers, controllers, database operations
- E2E Tests: Full API workflows
- Coverage Goal: β₯ 80% for business logic
- Framework: Jest with
@nestjs/testing
- Test files:
<module>.service.spec.ts
- Test suites:
describe('ServiceName', () => {})
- Test cases:
it('should do something', () => {})
import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
import { UserService } from './user.service';
import { User } from './user.schema';
import { ConflictException, NotFoundException } from '@nestjs/common';
describe('UserService', () => {
let service: UserService;
let mockUserModel: any;
const mockUser = {
_id: '123',
email: 'test@example.com',
name: 'Test User',
save: jest.fn().mockResolvedValue(this),
};
beforeEach(async () => {
mockUserModel = {
findById: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
new: jest.fn().mockResolvedValue(mockUser),
save: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: getModelToken(User.name),
useValue: mockUserModel,
},
],
}).compile();
service = module.get<UserService>(UserService);
});
describe('findById', () => {
it('should return user when found', async () => {
mockUserModel.findById.mockReturnValue({
exec: jest.fn().mockResolvedValue(mockUser),
});
const result = await service.findById('123');
expect(result).toEqual(mockUser);
expect(mockUserModel.findById).toHaveBeenCalledWith('123');
});
it('should throw NotFoundException when user not found', async () => {
mockUserModel.findById.mockReturnValue({
exec: jest.fn().mockResolvedValue(null),
});
await expect(service.findById('123')).rejects.toThrow(
NotFoundException,
);
});
});
describe('createUser', () => {
it('should create user successfully', async () => {
mockUserModel.findOne.mockResolvedValue(null);
mockUserModel.create.mockResolvedValue(mockUser);
const input = { email: 'new@example.com', name: 'New User' };
const result = await service.createUser(input);
expect(result).toEqual(mockUser);
expect(mockUserModel.findOne).toHaveBeenCalledWith({
email: input.email,
});
});
it('should throw ConflictException when email exists', async () => {
mockUserModel.findOne.mockResolvedValue(mockUser);
const input = { email: 'test@example.com', name: 'Test' };
await expect(service.createUser(input)).rejects.toThrow(
ConflictException,
);
});
});
});
import { Test, TestingModule } from '@nestjs/testing';
import { UserResolver } from './user.resolver';
import { UserService } from './user.service';
describe('UserResolver', () => {
let resolver: UserResolver;
let mockUserService: Partial<UserService>;
const mockUser = {
_id: '123',
email: 'test@example.com',
name: 'Test User',
};
beforeEach(async () => {
mockUserService = {
findAll: jest.fn().mockResolvedValue({
users: [mockUser],
total: 1,
}),
createUser: jest.fn().mockResolvedValue(mockUser),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UserResolver,
{
provide: UserService,
useValue: mockUserService,
},
],
}).compile();
resolver = module.get<UserResolver>(UserResolver);
});
describe('users query', () => {
it('should return paginated users', async () => {
const result = await resolver.users(1, 10);
expect(result.users).toHaveLength(1);
expect(result.total).toBe(1);
expect(mockUserService.findAll).toHaveBeenCalledWith({
page: 1,
limit: 10,
});
});
});
describe('createUser mutation', () => {
it('should create user successfully', async () => {
const input = { email: 'new@example.com', name: 'New User' };
const result = await resolver.createUser(input);
expect(result).toEqual(mockUser);
expect(mockUserService.createUser).toHaveBeenCalledWith(input);
});
});
});
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('UserResolver (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('should create user via GraphQL', () => {
return request(app.getHttpServer())
.post('/graphql')
.send({
query: `
mutation {
createUser(input: { email: "test@example.com", name: "Test" }) {
id
email
name
}
}
`,
})
.expect(200)
.expect((res) => {
expect(res.body.data.createUser).toHaveProperty('id');
expect(res.body.data.createUser.email).toBe('test@example.com');
});
});
});
- β
Use DTOs with
class-validator for all inputs
- β
Never expose sensitive data (passwords, tokens, API keys)
- β
Implement rate limiting using
@nestjs/throttler
- β
Sanitize inputs to prevent injection attacks
- β
Validate environment variables on application startup
- β
Use HTTPS for all external communications
- β
Implement CORS with strict origin policies
- β
Hash passwords using bcrypt (minimum 10 rounds)
- β
Use JWT with expiration for authentication
- β
Implement RBAC (Role-Based Access Control)
import {
IsEmail,
IsString,
MinLength,
MaxLength,
Matches
} from 'class-validator';
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class CreateUserInput {
@Field()
@IsEmail({}, { message: 'Invalid email format' })
email: string;
@Field()
@IsString()
@MinLength(8, { message: 'Password must be at least 8 characters' })
@MaxLength(50, { message: 'Password must not exceed 50 characters' })
@Matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
{ message: 'Password must contain uppercase, lowercase, number, and special character' }
)
password: string;
@Field()
@IsString()
@MinLength(2)
@MaxLength(50)
name: string;
}
import { plainToInstance } from 'class-transformer';
import {
IsString,
IsNumber,
IsEnum,
validateSync
} from 'class-validator';
enum Environment {
Development = 'development',
Production = 'production',
Test = 'test',
}
class EnvironmentVariables {
@IsEnum(Environment)
NODE_ENV: Environment;
@IsNumber()
PORT: number;
@IsString()
DATABASE_URL: string;
@IsString()
JWT_SECRET: string;
@IsNumber()
JWT_EXPIRATION: number;
}
export function validate(config: Record<string, unknown>) {
const validatedConfig = plainToInstance(EnvironmentVariables, config, {
enableImplicitConversion: true,
});
const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}
// app.module.ts
import { ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot({
ttl: 60,
limit: 10,
}),
],
})
export class AppModule {}
// Protected endpoint
import { Throttle } from '@nestjs/throttler';
@Resolver()
export class AuthResolver {
@Throttle(5, 60) // 5 requests per 60 seconds
@Mutation(() => AuthPayload)
async login(
@Args('email') email: string,
@Args('password') password: string,
): Promise<AuthPayload> {
return this.authService.login(email, password);
}
}
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const ctx = GqlExecutionContext.create(context);
const { req } = ctx.getContext();
const token = this.extractTokenFromHeader(req);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const payload = this.jwtService.verify(token);
req.user = payload;
return true;
} catch {
throw new UnauthorizedException('Invalid token');
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
import { SetMetadata } from '@nestjs/common';
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export enum UserRole {
Admin = 'admin',
User = 'user',
Guest = 'guest',
}
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// Usage
@Resolver()
export class UserResolver {
@Roles(UserRole.Admin)
@UseGuards(AuthGuard, RolesGuard)
@Mutation(() => Boolean)
async deleteUser(@Args('id') id: string): Promise<boolean> {
return this.userService.delete(id);
}
}
ΒΆ βοΈ 7. Git Commit & PR Standards
<type>(<scope>): <short summary> [<issue-id>]
feat(authentication): implement login mutation [QA-001]
fix(authentication): resolve invalid token refresh error [QA-045]
refactor(order): optimize aggregation pipeline [QA-083]
docs(api): update GraphQL usage guide [DOC-012]
- feat: New feature
- fix: Bug fix
- refactor: Code refactor
- docs: Documentation only
- test: Test-related changes
- chore: Maintenance tasks
- style: Non-functional style updates
- Summary should not exceed 100 characters
- Use lowercase for type and scope
- Always include [Issue-ID] if available (e.g., Jira, ClickUp)
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'header-max-length': [2, 'always', 100],
'subject-case': [2, 'always', ['sentence-case']],
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore'],
],
'scope-case': [2, 'always', 'kebab-case'],
},
};
[Type][Module] Short description [Issue-ID]
[Fix][Authentication] resolve invalid token refresh error [QA-045]
[Feat][User] add email verification mutation [QA-017]
### π§© Description
Explain what this PR does and why.
### π Issue Ref
QA-001
### β
Changes
- Added ...
- Fixed ...
- Refactored ...
### π§ͺ How to Test
1. Run ...
2. Open ...
3. Expected result ...
### π§ Notes
Any known issues, follow-ups, or context.
feature/<module>-<short-desc>
fix/<module>-<short-desc>
chore/<short-desc>
feature/user-login
fix/order-status-update
chore/update-dependencies
- Branch from develop
- Merge back via Pull Request
- All PRs require code review and passing tests
ΒΆ π¦ 9. API & GraphQL Standards
- Queries: Read-only operations (no side effects)
- Mutations: Create/update/delete actions with side effects
- Naming: Singular nouns for types (
User, Order, not Users)
- Pagination: Always implement for list queries
- Validation: All inputs must use DTO validation
- Documentation: Use descriptions in schema
- Nullable fields: Be explicit about nullable vs required fields
type User {
id: ID!
email: String!
name: String!
role: UserRole!
createdAt: DateTime!
updatedAt: DateTime!
}
enum UserRole {
ADMIN
USER
GUEST
}
type PaginatedUserResponse {
users: [User!]!
total: Int!
page: Int!
limit: Int!
hasMore: Boolean!
}
input CreateUserInput {
email: String!
password: String!
name: String!
}
input UpdateUserInput {
email: String
name: String
}
type Query {
"""Get all users with pagination"""
users(page: Int = 1, limit: Int = 10): PaginatedUserResponse!
"""Get user by ID"""
user(id: ID!): User
"""Get current authenticated user"""
me: User!
}
type Mutation {
"""Create a new user"""
createUser(input: CreateUserInput!): User!
"""Update existing user"""
updateUser(id: ID!, input: UpdateUserInput!): User!
"""Delete user by ID"""
deleteUser(id: ID!): Boolean!
}
import { ObjectType, Field, Int } from '@nestjs/graphql';
@ObjectType()
export class PaginatedUserResponse {
@Field(() => [User])
users: User[];
@Field(() => Int)
total: number;
@Field(() => Int)
page: number;
@Field(() => Int)
limit: number;
@Field()
hasMore: boolean;
}
// Service implementation
async findAll(page: number = 1, limit: number = 10): Promise<PaginatedUserResponse> {
const skip = (page - 1) * limit;
const [users, total] = await Promise.all([
this.userModel.find().skip(skip).limit(limit).exec(),
this.userModel.countDocuments().exec(),
]);
return {
users,
total,
page,
limit,
hasMore: skip + users.length < total,
};
}
ΒΆ GraphQL Error Handling
import { GraphQLError } from 'graphql';
import { ApolloError } from 'apollo-server-express';
export class CustomError extends ApolloError {
constructor(message: string, code: string, extensions?: Record<string, any>) {
super(message, code, extensions);
}
}
export class ValidationError extends CustomError {
constructor(message: string, fields?: Record<string, string>) {
super(message, 'VALIDATION_ERROR', { fields });
}
}
// Usage in service
if (!user) {
throw new ValidationError('User not found', { id: 'Invalid user ID' });
}
ΒΆ REST API Standards (if applicable)
GET /api/users # Get all users
GET /api/users/:id # Get user by ID
POST /api/users # Create user
PUT /api/users/:id # Update user (full)
PATCH /api/users/:id # Update user (partial)
DELETE /api/users/:id # Delete user
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
details?: any;
};
meta?: {
page: number;
limit: number;
total: number;
};
}
ΒΆ β 10. Error Handling
ΒΆ Error Handling Strategy
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { GqlExceptionFilter } from '@nestjs/graphql';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter, GqlExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
message =
typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message;
}
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message,
};
// Log the error
this.logError(exception, errorResponse);
response.status(status).json(errorResponse);
}
private logError(exception: unknown, errorResponse: any) {
console.error('Error:', {
...errorResponse,
stack: exception instanceof Error ? exception.stack : undefined,
});
}
}
import { HttpException, HttpStatus } from '@nestjs/common';
export class BusinessException extends HttpException {
constructor(message: string) {
super(message, HttpStatus.BAD_REQUEST);
}
}
export class ResourceNotFoundException extends HttpException {
constructor(resource: string, id: string) {
super(`${resource} with ID ${id} not found`, HttpStatus.NOT_FOUND);
}
}
export class DuplicateResourceException extends HttpException {
constructor(resource: string, field: string) {
super(
`${resource} with this ${field} already exists`,
HttpStatus.CONFLICT,
);
}
}
ΒΆ Service Error Handling
@Injectable()
export class UserService {
async findById(id: string): Promise<User> {
try {
const user = await this.userModel.findById(id).exec();
if (!user) {
throw new ResourceNotFoundException('User', id);
}
return user;
} catch (error) {
if (error instanceof ResourceNotFoundException) {
throw error;
}
this.logger.error(`Error finding user: ${error.message}`, error.stack);
throw new InternalServerErrorException('Failed to retrieve user');
}
}
}
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Schema as MongooseSchema } from 'mongoose';
export type UserDocument = User & Document;
@Schema({
timestamps: true,
collection: 'users'
})
export class User {
@Prop({ required: true, unique: true, lowercase: true })
email: string;
@Prop({ required: true, select: false }) // Don't return by default
password: string;
@Prop({ required: true })
name: string;
@Prop({
type: String,
enum: ['admin', 'user', 'guest'],
default: 'user'
})
role: string;
@Prop({ type: Boolean, default: true })
isActive: boolean;
@Prop({ type: Date })
lastLoginAt?: Date;
@Prop({ type: [{ type: MongooseSchema.Types.ObjectId, ref: 'Order' }] })
orders: MongooseSchema.Types.ObjectId[];
}
export const UserSchema = SchemaFactory.createForClass(User);
// Add indexes
UserSchema.index({ email: 1 });
UserSchema.index({ role: 1, isActive: 1 });
// Pre-save hook
UserSchema.pre('save', async function(next) {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 10);
}
next();
});
// β BAD - N+1 query problem
async getUsers() {
const users = await this.userModel.find();
for (const user of users) {
user.orders = await this.orderModel.find({ userId: user._id });
}
return users;
}
// β
GOOD - Use populate
async getUsers() {
return this.userModel
.find()
.populate('orders')
.select('-password') // Exclude sensitive fields
.lean() // Return plain objects for better performance
.exec();
}
// β
GOOD - Use aggregation for complex queries
async getUserStats() {
return this.userModel.aggregate([
{ $match: { isActive: true } },
{ $group: {
_id: '$role',
count: { $sum: 1 },
avgOrders: { $avg: { $size: '$orders' } }
}},
{ $sort: { count: -1 } }
]);
}
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
@Injectable()
export class OrderService {
constructor(
@InjectConnection() private connection: Connection,
@InjectModel(Order.name) private orderModel: Model<OrderDocument>,
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}
async createOrderWithTransaction(orderData: CreateOrderInput) {
const session = await this.connection.startSession();
session.startTransaction();
try {
const order = await this.orderModel.create([orderData], { session });
await this.userModel.findByIdAndUpdate(
orderData.userId,
{ $push: { orders: order[0]._id } },
{ session }
);
await session.commitTransaction();
return order[0];
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
}
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class UserService {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}
async findById(id: string): Promise<User> {
const cacheKey = `user:${id}`;
// Try to get from cache
const cached = await this.cacheManager.get<User>(cacheKey);
if (cached) {
return cached;
}
// If not in cache, fetch from DB
const user = await this.userModel.findById(id).lean().exec();
if (user) {
// Store in cache for 1 hour
await this.cacheManager.set(cacheKey, user, { ttl: 3600 });
}
return user;
}
async update(id: string, data: UpdateUserInput): Promise<User> {
const user = await this.userModel
.findByIdAndUpdate(id, data, { new: true })
.exec();
// Invalidate cache
await this.cacheManager.del(`user:${id}`);
return user;
}
}
// β BAD - Sequential operations
async getD ashboardData() {
const users = await this.userService.count();
const orders = await this.orderService.count();
const revenue = await this.orderService.getTotalRevenue();
return { users, orders, revenue };
}
// β
GOOD - Parallel operations
async getDashboardData() {
const [users, orders, revenue] = await Promise.all([
this.userService.count(),
this.orderService.count(),
this.orderService.getTotalRevenue(),
]);
return { users, orders, revenue };
}
// β BAD - Individual updates
async updateUsers(updates: Array<{id: string, data: any}>) {
for (const update of updates) {
await this.userModel.findByIdAndUpdate(update.id, update.data);
}
}
// β
GOOD - Bulk write
async updateUsers(updates: Array<{id: string, data: any}>) {
const bulkOps = updates.map(update => ({
updateOne: {
filter: { _id: update.id },
update: { $set: update.data }
}
}));
return this.userModel.bulkWrite(bulkOps);
}
- β
Follows naming conventions
- β
Functions are < 50 lines (prefer smaller)
- β
No duplicate code (DRY principle)
- β
Proper error handling implemented
- β
No
console.log or commented code
- β
No unused imports or variables
- β
Proper TypeScript types (no
any)
- β
DTO validation present for all inputs
- β
Null/undefined handled explicitly
- β
Type guards used where necessary
- β
Business logic in services, not resolvers
- β
Proper separation of concerns
- β
Dependencies injected correctly
- β
Modules organized properly
- β
Queries optimized (no N+1 problems)
- β
Indexes defined for frequently queried fields
- β
Transactions used for multi-document operations
- β
Sensitive data excluded from queries
- β
Authentication/authorization implemented
- β
Input validation comprehensive
- β
No sensitive data in logs
- β
Rate limiting on sensitive endpoints
- β
Unit tests written and passing
- β
Test coverage β₯ 80%
- β
Edge cases covered
- β
Mock dependencies properly
- β
TSDoc comments for public APIs
- β
Complex logic explained
- β
README updated if needed
- β
Commit messages follow format
- β
Lint checks pass
- β
Build successful
- β
No breaking changes (or documented)
- β
Migration scripts included if needed
ΒΆ π 14. Deployment Standards
// .env.example - Commit this file
NODE_ENV=development
PORT=3000
DATABASE_URL=mongodb://localhost:27017/dbname
JWT_SECRET=your-secret-key
JWT_EXPIRATION=3600
REDIS_HOST=localhost
REDIS_PORT=6379
// .env - NEVER commit this file
# .github/workflows/deploy.yml
name: Deploy Backend
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:6.0
ports:
- 27017:27017
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test:cov
env:
DATABASE_URL: mongodb://localhost:27017/test
- name: Build
run: npm run build
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to Production
run: |
# Add your deployment script here
echo "Deploying to production..."
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=mongodb://mongo:27017/prod
depends_on:
- mongo
- redis
mongo:
image: mongo:6.0
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
mongo-data:
import { Controller, Get } from '@nestjs/common';
import {
HealthCheck,
HealthCheckService,
MongooseHealthIndicator
} from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: MongooseHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database', { timeout: 300 }),
]);
}
}
- β
Environment variables validated and injected
- β
Secrets managed via secure vault (not in code)
- β
Database migrations run automatically
- β
Health check endpoint operational
- β
Logging configured for production
- β
Error tracking connected (Sentry)
- β
Monitoring enabled (Prometheus/Grafana)
- β
Backups configured and tested
- β
SSL/TLS certificates valid
- β
Rate limiting enabled
Ensure visibility, traceability, and reliability of backend systems by implementing structured logging, error tracking, and application monitoring.
ΒΆ Logging Standards
| Aspect |
Guideline |
| Library |
Use pino or winston for structured JSON logging |
| Format |
Log entries must include timestamp, level, context, and message |
| Levels |
error, warn, info, debug, verbose |
| Location |
Use common/logger as centralized logger utility |
| Environment |
Pretty-print logs in development, JSON format in production |
| Sensitive Data |
Never log passwords, tokens, or PII |
import { LoggerService } from '@nestjs/common';
import * as pino from 'pino';
export class AppLogger implements LoggerService {
private readonly logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport:
process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
});
log(message: string, context?: string) {
this.logger.info({ context }, message);
}
error(message: string, trace?: string, context?: string) {
this.logger.error({ context, trace }, message);
}
warn(message: string, context?: string) {
this.logger.warn({ context }, message);
}
debug(message: string, context?: string) {
this.logger.debug({ context }, message);
}
}
@Injectable()
export class UserService {
private readonly logger = new AppLogger();
async createUser(input: CreateUserInput) {
this.logger.log('Creating new user', UserService.name);
try {
const user = await this.userModel.create(input);
this.logger.info(`User created with id ${user._id}`);
return user;
} catch (err) {
this.logger.error('Error creating user', err.stack);
throw err;
}
}
}
| Category |
Tool |
Purpose |
| Application Metrics |
Prometheus + Grafana |
Monitor CPU, memory, response time |
| Error Tracking |
Sentry / New Relic |
Capture and analyze runtime exceptions |
| Health Checks |
/health endpoint + @nestjs/terminus |
Ensure service availability |
| Tracing |
OpenTelemetry (OTEL) |
Track request flows across microservices |
| Log Aggregation |
ELK Stack or Loki |
Centralize and visualize logs |
- Configure alerts for:
- Error rate > 5% within 5 minutes
- Memory usage > 80%
- Response time > 2 seconds (avg)
- Integrate alerts with Slack / Email / PagerDuty
- Critical alerts should automatically trigger incident creation
| Level |
Usage |
| error |
Application errors or failed operations |
| warn |
Potential issues, deprecated usage |
| info |
Normal operations, service startup, shutdown |
| debug |
Debugging during development or troubleshooting |
| verbose |
Highly detailed flow (disabled in production) |
| Category |
Tool |
Purpose |
| Framework |
NestJS v10+ |
Backend framework |
| Language |
TypeScript |
Type-safe development |
| Database |
MongoDB + Mongoose |
NoSQL database |
| GraphQL |
Apollo Server |
API layer |
| Validation |
class-validator + class-transformer |
DTO validation |
| Category |
Tool |
Purpose |
| Linting |
ESLint + Prettier |
Code quality |
| Git Hooks |
Husky + Commitlint |
Enforce standards |
| Testing |
Jest |
Unit & integration testing |
| Documentation |
Compodoc / Swagger |
API documentation |
| Type Checking |
TypeScript Compiler |
Static analysis |
| Tool |
Purpose |
| GitHub Actions / GitLab CI |
Continuous integration |
| Docker |
Containerization |
| Kubernetes |
Orchestration (optional) |
| Category |
Tool |
Purpose |
| Error Tracking |
Sentry / New Relic |
Runtime errors |
| Logging |
Pino / Winston |
Structured logging |
| Metrics |
Prometheus + Grafana |
Performance monitoring |
| APM |
New Relic / Datadog |
Application performance |
| Tracing |
OpenTelemetry |
Distributed tracing |
# .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit $1
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint
npm run test:changed
- Install dependencies and set up environment variables
- Read
README.md and BE_GUIDELINES.md
- Run unit tests and ensure they pass
- Set up local MongoDB/Redis or use docker-compose
- Check logs for errors (
pino/winston output)
- Verify environment variable validation
- Use health check endpoint
/health
- Inspect database connectivity and indexes
ΒΆ π§Ή 19. Maintenance Rules
- Keep dependencies updated monthly
- Archive deprecated modules with clear notices
- Maintain migration scripts for schema changes
- Ensure backward compatibility or document breaking changes
- Start:
npm run start:dev
- Test:
npm run test
- Lint:
npm run lint
- Build:
npm run build
|
|
| Last Updated |
October 2025 |
| Version |
2.0 |
| Maintained by |
Backend Core Team |
| Applies to |
All backend services using NestJS, GraphQL, MongoDB, or REST |
| Review Cycle |
Quarterly |
| Next Review |
January 2026 |
- v2.0 (Oct 2025): Major restructure with comprehensive examples and new sections
- v1.5 (Jul 2025): Added Testing and Security sections
- v1.0 (Jan 2025): Initial version
To propose changes to these guidelines:
- Create a branch:
docs/update-be-guidelines-<topic>
- Make your changes to
BE_GUIDELINES.md
- Submit a PR with detailed reasoning
- Get approval from at least 2 senior backend developers
- Update version number and document history
"Consistency is the foundation of scalability β follow the rules, improve the rules."
- Quality over Speed: Take time to do it right
- Security First: Never compromise on security
- Simplicity over Complexity: Keep it simple and maintainable
- Collaboration: Code review and pair programming
- Continuous Learning: Stay updated with best practices
- β
Code Quality: Clean, maintainable, well-documented code
- β
Performance: Fast, efficient, scalable services
- β
Security: No vulnerabilities, proper authentication
- β
Testing: High coverage, reliable test suites
- β
Reliability: High uptime, quick recovery from failures
- β
Developer Experience: Easy to understand and work with
- These are guidelines, not rigid rules
- Use good judgment for exceptions
- Discuss with team when in doubt
- Update guidelines as we learn and evolve
- Help each other grow and improve
Happy Coding! πβ