Skip to content

Coding Rules

Shared coding standards, conventions, and best practices for the Guinness backend (api-admin / api-user).


Core Principles

  1. Type safety โ€” Use strict TypeScript; avoid any
  2. Explicitness โ€” Prefer explicit types over implicit inference when it improves readability
  3. Consistency โ€” Follow existing patterns in the codebase
  4. 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

Routes (API layer)
    โ†“
Services (Business logic)
    โ†“
Repositories (Data access)
    โ†“
Database
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

// โœ“ Preferred
type Status = 'pending' | 'processing' | 'completed' | 'failed';

Immutability

// โœ“ Good
const updatedUser = { ...user, name: 'New Name' };

// โœ— Avoid
user.name = 'New Name';

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__/):

src/services/user.ts  โ†’  user.test.ts (co-located)
                     or
__tests__/unit/services/user.test.ts

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 any types 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