Coding Rules
Shared coding standards, conventions, and best practices for the Guinness backend (api-admin / api-user).
Core Principles
- Type safety โ Use strict TypeScript; avoid
any - Explicitness โ Prefer explicit types over implicit inference when it improves readability
- Consistency โ Follow existing patterns in the codebase
- Simplicity โ Write clear, maintainable code over clever code
Directory Structure
apps/<app>/src/
โโโ config/ # Configuration modules
โโโ middlewares/ # Middleware functions
โโโ routes/ # API route handlers
โ โโโ v1/ # Version 1 routes
โโโ services/ # Business logic
โโโ repositories/ # Data access layer
โโโ lib/ # Shared library code
โโโ utils/ # Utility functions
โโโ types/ # Type definitions
โ โโโ endpoint/ # Endpoint-specific types
โ โโโ zod-openapi/ # Zod schemas and OpenAPI
โ โโโ components/ # Reusable schemas
โ โโโ routes/ # Route-specific schemas
โโโ app.ts # Application initialization
โโโ server.ts # Server configuration
โโโ error.ts # Error definitions
Layered Architecture
| Layer | Responsibility | Example |
|---|---|---|
| Routes | HTTP handling, validation, response formatting | src/routes/v1/user.ts |
| Services | Business logic, orchestration | src/services/user.ts |
| Repositories | Database operations | src/repositories/user.ts |
| Types | Type definitions, schemas | src/types/zod-openapi/routes/user.ts |
| Middlewares | Request / response processing | src/middlewares/protected-route.ts |
| Config | Configuration management | src/config/database.ts |
| Utils | Pure utility functions | src/utils/pagination.ts |
Naming Conventions
File Names
Use kebab-case for all file names. Test files use the same name with a .test.ts suffix.
โ user-service.ts
โ auth-middleware.ts
โ user-service.test.ts
โ userService.ts
โ user_service.ts
Variables
| Kind | Case | Example |
|---|---|---|
| True constants | UPPER_SNAKE_CASE |
MAX_FILE_SIZE, API_BASE_URL |
| Config objects | camelCase |
databaseConfig |
| Local variables | camelCase |
userId, isAdmin |
Functions
Use camelCase starting with a verb. Do not add async / Async suffixes to async functions.
| Verb | Purpose | Example |
|---|---|---|
find |
Fetch multiple records | findAll, findById |
findOne |
Fetch a single record | findOneById |
create |
Create new entity | createUser |
update |
Update entity | updateProject |
softDelete |
Logical delete | softDelete |
is / has / can |
Boolean check | isAdmin, hasPermission |
Classes, Interfaces, and Types
Use PascalCase. Do not prefix interfaces with I.
interface User { }
interface CreateUserData { }
class NotFoundError extends Error { }
type Status = 'pending' | 'active';
API
| Target | Case | Example |
|---|---|---|
| Endpoint paths | kebab-case resource names | /v1/des2code |
| Path parameters | snake_case (singular) | :organization_id |
| Query parameters | snake_case | ?created_after= |
| JSON keys | snake_case | "organization_id" |
Database
| Target | Case | Example |
|---|---|---|
| Table names | snake_case (plural) | users, generated_codes |
| Column names | snake_case | cognito_sub, created_at |
TypeScript
tsconfig.json (required settings)
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true
}
}
Type Definitions
Use interface for object shapes, public API contracts, and data models. Use type for union types, utility types, and function signatures.
// interface
interface User { id: string; name: string; }
interface AdminUser extends User { roleId: string; }
// type
type Status = 'pending' | 'processing' | 'completed' | 'failed';
type CreateUserData = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
No any
// โ Bad
function processData(data: any) { return data.value; }
// โ Good
function processData(data: Record<string, unknown>) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return data.value;
}
throw new Error('Invalid data format');
}
Use unknown for truly unknown types, then narrow with type guards.
function handleError(error: unknown): void {
if (error instanceof Error) {
logger.error('Error occurred', { message: error.message });
} else {
logger.error('Unknown error', { error: String(error) });
}
}
Null Handling
// โ Bad
function getUserName(user: User) { return user.name.toUpperCase(); }
// โ Good
function getUserName(user: User): string {
return user.name?.toUpperCase() ?? 'Unknown';
}
Return Types
All functions and async functions must have explicit return types.
// โ Good
export async function findUser(id: string): Promise<User | null> { }
// โ Avoid
export async function findUser(id: string) { }
Prefer Union Types over Enums
Immutability
Import Rules
Import Order
// 1. External libraries
import { OpenAPIHono } from '@hono/zod-openapi';
// 2. Internal absolute imports (@ alias)
import { logger } from '@GenAI-Guinness-backend/utils';
import * as userService from '@/services/user';
// 3. Relative imports
import { formatOutput } from './utils';
// 4. Type imports
import type { User } from '@/repositories/user';
Path Aliases
Use @/ for absolute imports from src/. Avoid deep relative paths like ../...
// โ Good
import * as userService from '@/services/user';
// โ Avoid
import * as userService from '../../services/user';
Error Handling
Typed Error Classes
export class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'NotFoundError';
}
}
export class ForbiddenError extends Error {
constructor(message: string) {
super(message);
this.name = 'ForbiddenError';
}
}
Layer Responsibilities
// Routes โ log and re-throw
try {
const user = await userService.findOneById(userId, orgId, requester);
return c.json(formatOutput(user));
} catch (error) {
logger.error('Failed to fetch user', { error, userId });
throw error;
}
// Services โ throw typed errors
if (!user) throw new NotFoundError(`User ${userId} not found`);
if (!hasPermission(requester, user)) throw new ForbiddenError('Insufficient permissions');
Common Patterns
CRUD Function Names
| Layer | Operation | Function |
|---|---|---|
| Service | List | find() |
| Service | Get by ID | findOneById() |
| Service | Create | create() |
| Service | Update | update() |
| Service | Soft delete | softDelete() |
| Repository | SELECT by ID | findById() |
| Repository | SELECT all | findAll() |
| Repository | INSERT | insert() |
| Repository | UPDATE | update() |
| Repository | Soft delete | softDelete() |
Pagination
export async function find(
organizationId: string,
requesterUsername: string,
options: PaginationOptions
): Promise<PaginatedResponse<User>> {
const { page, limit, sort } = options;
const offset = pagination.calculateOffset(page, limit);
const users = await userRepository.findAll({ organizationId, limit, offset, sort });
return { currentPage: page, totalCount: users.total, list: users.items };
}
Testing
File Placement
Place test files next to source files (or inside __tests__/):
Test Structure
import { describe, it, expect } from 'bun:test';
describe('User Service', () => {
describe('findById()', () => {
it('should return user when found', async () => {
// Arrange
const userId = 'user-123';
// Act
const user = await userService.findById(userId);
// Assert
expect(user).toBeDefined();
expect(user?.id).toBe(userId);
});
});
});
Commands
bun test # Run all tests
bun test src/services/user.test.ts # Run a specific file
bun test --coverage # Run with coverage
bun run type-check # Type check
bun run lint # Run linter
bun run lint:fix # Auto-fix lint issues
Code Review Checklist
Type Safety
- [ ] No
anytypes used - [ ] All functions have explicit return types
- [ ] null / undefined handled appropriately
Naming
- [ ] Files use kebab-case
- [ ] Variables use camelCase; constants use UPPER_SNAKE_CASE
- [ ] Functions start with a verb
- [ ] Types / interfaces use PascalCase
Design
- [ ] Code is in the correct layer (routes / services / repositories)
- [ ] Imports are properly ordered and use
@/aliases - [ ] Named exports only (no default exports)
Error Handling
- [ ] Errors are typed
- [ ] Errors are logged with context
- [ ] try-catch is used at the appropriate layer
Testing
- [ ] Unit tests exist for services
- [ ] Integration tests exist for routes
- [ ] Edge cases and error cases are covered