Repository navigation
Backend Coding Standards
Canonical source:
docs/04-backend/coding-standards.md— this page is rendered from it on every publish; edit the source.
Mandatory coding rules, patterns, and quality requirements for all 13 ClawAI NestJS backend services.
- Layer Architecture
- Controller Rules
- Service Rules
- Manager Rules
- Repository Rules
- Error Handling
- DTO and Validation
- File Organization and Extraction Rules
- ESLint Rules Summary
- Library Wrapping Rule
- Absolute Code Rules
- Testing Requirements
- Code Quality Checklist
Every backend service follows a strict layered architecture:
Controller -> Service -> Repository (data access only)
-> Manager (complex logic, external calls)
Each layer has specific responsibilities and constraints. Violations of these boundaries produce maintainability and testability issues.
- Controllers depend on Services only (never Repositories or Managers directly)
- Services depend on Repositories and/or Managers
- Managers depend on Repositories and/or external APIs
- Repositories depend on Prisma Client or Mongoose only
- No circular dependencies between layers
Controllers are the thinnest possible layer. They exist only to map HTTP requests to service calls.
- 3-line methods ONLY: Extract parameters, call ONE service method, return the result
-
NO try/catch -- errors are handled by the
GlobalExceptionFilter - NO throw statements -- do not throw exceptions directly
- NO business logic -- delegate everything to the service layer
- NO direct database access -- never import repositories
- ONE service call per endpoint -- never orchestrate multiple service calls
-
NO inline type definitions -- extract to
types/files
@Post()
async create(
@CurrentUser() user: AuthenticatedUser,
@Body(new ZodValidationPipe(createSchema)) dto: CreateDto,
): Promise<Entity> {
return this.entityService.create(user.id, dto);
}// BAD: Business logic in controller
@Post()
async create(@Body() dto: CreateDto): Promise<Entity> {
const existing = await this.service.findByName(dto.name);
if (existing) {
throw new ConflictException('Already exists'); // NO throws
}
const result = await this.service.create(dto);
await this.auditService.log('created', result); // NO multiple service calls
return result;
}Services contain business logic and orchestrate data access through repositories.
- Max 30 lines per method -- if longer, extract helper methods or delegate to a Manager
- Split complex methods into smaller private methods
- Each public method does ONE thing
- Validate ownership/permissions here (not in controller or repository)
- Publish events here (not in controller)
-
Max complexity of 10 (ESLint
complexityrule)
When a service method exceeds 30 lines:
- Extract validation logic to a private
validateX()method - Extract transformation/mapping logic to a private
buildX()ormapX()method - Move complex orchestration (multiple external calls, retries) to a Manager class
- Extract reusable logic to utility files
async createMemory(userId: string, dto: CreateMemoryDto): Promise<MemoryRecord> {
const record = await this.memoryRepository.create(userId, dto);
await this.publishMemoryCreated(record);
return record;
}
private async publishMemoryCreated(record: MemoryRecord): Promise<void> {
await this.rabbitMQ.publish(EventPattern.MEMORY_EXTRACTED, {
memoryId: record.id,
userId: record.userId,
type: record.type,
content: record.content,
timestamp: new Date().toISOString(),
});
}Managers handle complex orchestration that would make services too large.
- Max 80 lines per method, complexity limit 15
- Each private helper should be <30 lines
-
Clear naming:
buildPromptString(),fetchConnectorConfig(),parseResponse() - Handle retries and fallbacks here
- External API calls go through managers, not services directly
- Multiple service calls that must be orchestrated
- External API interactions with retry logic
- Complex data transformation pipelines
- Operations that require fallback chains (e.g.,
ChatExecutionManager,ContextAssemblyManager)
Repositories are the sole data access layer. They are thin wrappers around Prisma or Mongoose queries.
- Pure data access ONLY -- no business logic
-
NO throw statements -- return data or
null, let services decide error handling - Each method maps to ONE database operation
- Use Prisma/Mongoose query builders -- no raw SQL
- No event publishing -- repositories are data-only
async findById(id: string): Promise<Entity | null> {
return this.prisma.entity.findUnique({ where: { id } });
}
async findByUserId(userId: string, page: number, limit: number): Promise<PaginatedResult<Entity>> {
const [data, total] = await Promise.all([
this.prisma.entity.findMany({
where: { userId },
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
}),
this.prisma.entity.count({ where: { userId } }),
]);
return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) } };
}All domain errors use BusinessException with a machine-readable code string.
import { BusinessException } from '../common/errors/business.exception';
import { HttpStatus } from '@nestjs/common';
// Entity not found
throw new EntityNotFoundException('Memory', id);
// Business rule violation
throw new BusinessException(
'Cannot deactivate yourself',
HttpStatus.FORBIDDEN,
'CANNOT_DEACTIVATE_SELF',
);
// Generic bad request
throw new BusinessException('Invalid configuration', HttpStatus.BAD_REQUEST, 'INVALID_CONFIG');| Exception Class | HTTP Status | Usage |
|---|---|---|
BusinessException |
Varies (400, 403, 409, etc.) | General business rule violations |
EntityNotFoundException |
404 | Entity not found by ID |
UnauthorizedException |
401 | Missing or invalid JWT token |
ForbiddenException |
403 | Insufficient permissions (RBAC) |
- NEVER swallow errors silently -- always log and rethrow or handle explicitly
-
NEVER use try/catch in controllers -- the
GlobalExceptionFilterhandles all errors -
NEVER throw in repositories -- return
nulland let services throwEntityNotFoundException -
Always include a machine-readable
codeinBusinessException - Log errors with context -- include relevant IDs (userId, entityId, threadId)
{
"statusCode": 404,
"message": "Memory with ID abc-123 not found",
"code": "ENTITY_NOT_FOUND",
"timestamp": "2026-04-09T12:00:00.000Z",
"path": "/api/v1/memories/abc-123"
}All input validation uses Zod schemas. Every service uses Zod exclusively -- class-validator and class-transformer are not used.
-
Every
z.string()MUST have.max()for length limits -
Every
z.array()MUST have.max()for size limits -
Schemas go in
src/modules/<domain>/dto/<name>.dto.ts - Export both the schema and the inferred type
// src/modules/memory/dto/create-memory.dto.ts
import { z } from 'zod';
import { MemoryType } from '../../../common/enums';
export const createMemorySchema = z.object({
type: z.nativeEnum(MemoryType),
content: z.string().min(1).max(10000),
sourceThreadId: z.string().uuid().optional(),
sourceMessageId: z.string().uuid().optional(),
});
export type CreateMemoryDto = z.infer<typeof createMemorySchema>;// Method-level (preferred for single DTO)
@Post()
async create(
@Body(new ZodValidationPipe(createSchema)) dto: CreateDto,
): Promise<Entity> { ... }
// Method-level with @UsePipes (when applied to entire method)
@Post()
@UsePipes(new ZodValidationPipe(createSchema))
async create(@Body() dto: CreateDto): Promise<Entity> { ... }All list endpoints follow this pattern:
export const listQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(DEFAULT_PAGE),
limit: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
search: z.string().max(200).optional(),
});
export type ListQueryDto = z.infer<typeof listQuerySchema>;src/
app/
config/ # AppConfig (Zod-validated env)
decorators/ # Custom decorators (@Public, @Roles, @CurrentUser)
filters/ # GlobalExceptionFilter
guards/ # AuthGuard, RolesGuard
interceptors/ # Logging, timing interceptors
pipes/ # ZodValidationPipe
common/
constants/ # Shared constants for this service
enums/ # Service-specific enums
errors/ # BusinessException, EntityNotFoundException
types/ # Common types (AuthenticatedUser, PaginatedResult)
utilities/ # Library wrappers (jwt.utility.ts, hashing.utility.ts)
modules/
<domain>/
controllers/ # HTTP controllers
dto/ # Zod schemas + inferred types
repositories/ # Data access (Prisma/Mongoose)
services/ # Business logic
managers/ # Complex orchestration (optional)
types/ # Domain-specific types
constants/ # Domain-specific constants
generated/
prisma/ # Auto-generated Prisma client
Code elements MUST be extracted to their designated locations. Inline definitions in logic files are forbidden.
| Element | Destination | Rule |
|---|---|---|
| Types / Interfaces | src/modules/<domain>/types/<name>.types.ts |
NO inline TSInterfaceDeclaration or TSTypeAliasDeclaration in logic files |
| Enums | src/common/enums/<name>.enum.ts |
NO inline TSEnumDeclaration in logic files |
| Constants |
src/common/constants/<name>.constants.ts or src/modules/<domain>/constants/
|
NO top-level const in logic files |
| Utilities | src/common/utilities/<name>.utility.ts |
NO standalone FunctionDeclaration in logic files |
| DTOs | src/modules/<domain>/dto/<name>.dto.ts |
Always co-locate schema and type |
| Errors | src/common/errors/ |
Custom exception classes |
| Pipes | src/app/pipes/ |
Validation pipes |
| Guards | src/app/guards/ |
Auth and role guards |
| Filters | src/app/filters/ |
Exception filters |
| Interceptors | src/app/interceptors/ |
Logging, timing |
| Decorators | src/app/decorators/ |
Custom parameter/method decorators |
String literal unions ('a' | 'b') are forbidden. Always use enums:
// BAD
type Status = 'active' | 'inactive';
// GOOD
enum Status {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
}Every backend service shares an identical ESLint configuration. ESLint 9 flat config with 4 plugin groups.
| Rule | Description |
|---|---|
no-explicit-any |
Use unknown, generics, or proper types instead of any
|
no-unused-vars |
No unused variables (except _ prefixed) |
no-non-null-assertion |
No ! operator -- handle nullability explicitly |
no-floating-promises |
All promises must be awaited or voided |
no-misused-promises |
No promises in boolean positions |
default-param-last |
Default parameters must be last |
no-useless-empty-export |
No empty export statements |
no-loop-func |
No function creation in loops |
return-await |
Only use return await inside try-catch |
| Rule | Description |
|---|---|
consistent-type-imports |
Prefer import type with inline style |
explicit-function-return-type |
All functions must have explicit return types |
prefer-nullish-coalescing |
Use ?? over || for nullish values |
prefer-optional-chain |
Use ?. over manual null checks |
no-shadow |
No variable shadowing |
| Rule | Description |
|---|---|
no-console |
Only console.warn and console.error allowed (use NestJS Logger) |
eqeqeq |
Always use === and !==
|
no-var |
Use const or let only |
prefer-const |
Use const when not reassigned |
no-eval / no-implied-eval / no-new-func
|
No dynamic code execution |
prefer-template |
Use template literals over string concatenation |
no-param-reassign |
No parameter reassignment (props: false) |
| Level | Rules |
|---|---|
| Errors | detect-eval-with-expression, detect-no-csrf, detect-buffer-noassert, detect-disable-mustache-escape, detect-new-buffer |
| Warnings | detect-object-injection, detect-non-literal-regexp, detect-timing-attacks, detect-non-literal-fs, detect-child-process, detect-pseudoRandomBytes, detect-unsafe-regex |
| Level | Rules |
|---|---|
| Errors | prefer-node-protocol, no-nested-ternary, prefer-string-slice |
| Warnings | no-for-each, no-useless-undefined, prefer-ternary, prefer-array-find/some/includes, prefer-number-properties, no-lonely-if, no-array-push-push, prefer-spread, prefer-string-replace-all, prefer-at |
| Rule | Description |
|---|---|
no-duplicates |
No duplicate imports (prefer-inline) |
first |
Imports must come first |
newline-after-import |
Blank line after import block |
no-mutable-exports |
No mutable exported bindings |
no-self-import |
No self-importing modules |
no-useless-path-segments |
No unnecessary path segments |
| File Type | Max Lines/Method | Max Complexity |
|---|---|---|
*.service.ts |
50 | 10 |
*.manager.ts |
80 | 15 |
*.controller.ts |
No limit (3-line convention) | No try/catch, no throw |
*.repository.ts |
No limit | No throw |
*.spec.ts |
All restrictions OFF |
any allowed |
Every third-party library MUST be wrapped in a utility file. Services and controllers NEVER import third-party packages directly.
- If the library changes or is replaced, only the wrapper file needs updating
- Provides a consistent API surface across the codebase
- Enables easier testing through mock injection
- Centralizes configuration
src/common/utilities/<library-name>.utility.ts
| Library | Wrapper File | Exports |
|---|---|---|
argon2 |
hashing.utility.ts |
hashPassword(), verifyPassword()
|
jsonwebtoken |
jwt.utility.ts |
signToken(), verifyToken()
|
crypto (AES-256-GCM) |
encryption.utility.ts |
encrypt(), decrypt()
|
ioredis |
redis.utility.ts |
Redis client factory |
axios / fetch
|
http.utility.ts |
httpGet(), httpPost()
|
// BAD: Direct third-party import in service
import * as argon2 from 'argon2';
// GOOD: Import from wrapper
import { hashPassword, verifyPassword } from '../common/utilities/hashing.utility';These rules apply to ALL files in ALL backend services. Zero tolerance for violations.
| # | Rule | Rationale |
|---|---|---|
| 1 | NEVER use any
|
Use unknown, generics, or proper types |
| 2 | NEVER disable ESLint rules | Fix the underlying issue |
| 3 | NEVER use string literal unions | Use enums from src/enums/ or src/common/enums/
|
| 4 | NEVER compare domain values with raw strings | Use enum comparisons |
| 5 | NEVER log secrets, tokens, API keys, passwords | Security violation |
| 6 | NEVER allow missing explicit return types | All functions typed |
| 7 | NEVER build god-files | Keep modules focused and small |
| 8 | NEVER define types/interfaces/enums/constants inline | Extract to dedicated files |
| 9 | NEVER put business logic in controllers | 3-line methods only |
| 10 | NEVER put Prisma/Mongoose calls outside repositories | Repositories are the sole data layer |
| 11 | NEVER cross database boundaries | Use RabbitMQ or HTTP for inter-service data |
| 12 | Each service owns its data | No shared databases |
| 13 | Use type over interface
|
Unless declaration merging is needed |
| 14 | NEVER use process.env directly |
Use AppConfig (Zod-validated) |
| 15 | NEVER use console.log
|
Use NestJS Logger |
| 16 | NEVER use ! non-null assertion |
Handle nullability explicitly |
| 17 | NEVER use == or !=
|
Always use === and !==
|
| 18 | NEVER use var
|
Use const, or let only when reassignment required |
| 19 | NEVER add features without tests | Every new function needs a test |
| 20 | NEVER add user-facing text without i18n | Extract to translation files |
- Every new function requires a corresponding test
- Tests go in
*.spec.tsfiles co-located with the source or in a__tests__/directory - All ESLint restrictions are OFF in test files --
anyis allowed - Use Vitest as the testing framework for all backend services (
vitest run; ADR-099).Mocked<T>andvicome fromvitest
describe('MemoryService', () => {
let service: MemoryService;
let repository: Mocked<MemoryRepository>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
MemoryService,
{ provide: MemoryRepository, useValue: createMock<MemoryRepository>() },
],
}).compile();
service = module.get(MemoryService);
repository = module.get(MemoryRepository);
});
describe('createMemory', () => {
it('should create and return a memory record', async () => {
// Arrange
const dto = { type: MemoryType.FACT, content: 'Test content' };
repository.create.mockResolvedValue(mockMemory);
// Act
const result = await service.createMemory('user-id', dto);
// Assert
expect(result).toEqual(mockMemory);
expect(repository.create).toHaveBeenCalledWith('user-id', dto);
});
});
});| Layer | What to Test | What to Mock |
|---|---|---|
| Controller | HTTP status codes, parameter extraction, service delegation | Service methods |
| Service | Business logic, validation, error conditions, event publishing | Repository, RabbitMQ, Managers |
| Manager | Orchestration logic, retry behavior, fallback chains | External APIs, Repositories |
| Repository | Query construction, pagination math | Prisma/Mongoose client |
npm run test # Run all tests across all workspaces
npm run test -- --watch # Watch mode
npm run test -- --coverage # With coverage reportUse this checklist before submitting any code change:
- Understand which service owns the domain
- Check if types/enums already exist in shared packages or common directories
- Identify the correct layer (controller/service/manager/repository)
- All functions have explicit return types
- No
anytypes anywhere - All strings validated with
.max()in Zod schemas - All arrays validated with
.max()in Zod schemas - No inline type/enum/constant definitions
- Third-party libraries accessed through wrappers only
- No
process.env-- using AppConfig - No
console.log-- using NestJS Logger - No raw string comparisons for domain values -- using enums
- Error handling uses
BusinessExceptionwith acode - Controllers are 3-line methods with no logic
- Service methods are under 30 lines
- Manager methods are under 80 lines
- Repositories have no throw statements
-
npm run lintpasses with 0 errors -
npm run typecheckpasses with 0 errors -
npm run buildsucceeds -
npm run testpasses (all tests green) - New tests written for new functionality
- Prisma migrations created if schema changed
- Environment variables added to
.env.example,.env, and install scripts - i18n translations added for all 9 locales if user-facing text
- Documentation updated if architecture affected
-
CLAUDE.mdupdated if new patterns or rules introduced
ClawAI Wiki · source-controlled under wiki/ · generated/mirrored from the current repository knowledge layer.
ClawAI Wiki
Product
- Product-Vision
- Flagship-Features
- Business-Overview
- Requirements-Register
- Drift-Log
- Runtime-Managed-Connector-Providers
- Threads
- ADR-Index
Architecture
- System-at-a-Glance
- Architecture-Map
- System-Architecture
- Message-Flow
- End-to-End-Data-Flow
- Routing-Engine
- Event-Bus
- Security-Architecture
Platform
- Backend-Services
- Frontend-Architecture
- Shared-Packages
- Database-Reference
- API-Reference
- Provider-Catalog
- Memory-and-Context-Architecture
- File-Support-Architecture
- Research-and-Web-Crawling
Runtime & Ops
- Docker-Guide
- Nginx-Reference
- CI-CD-Pipeline
- Logging-and-Observability
- Operational-Runbooks
- Troubleshooting
Quality
Engineering OS
- AI-Native-Engineering-OS
- Generated-Manifests
- Rules-Catalog
- Skills-Catalog
- Context-Layer
- Engineering-Memory
- Tooling-and-Automation
- Scripts-and-Operations
- ESLint-and-Architecture-Enforcement
- Extended-Work-Skills
Agents
Reference