Skip to main content

Overview

Aya maintains strict coding standards to ensure consistency, readability, and maintainability. These rules are enforced through automated linting and pre-commit hooks.
All code must pass make ok before committing. This runs all linters and formatters for both frontend and backend.

Critical Conventions

These rules are non-negotiable and enforced by linters:

Explicit Null/Undefined Checks

Never use truthy/falsy checks except for booleans. Always use explicit comparisons.
// Explicit null/undefined checks
if (value === null) {}
if (value !== undefined) {}
if (user === null) { return; }

// Explicit empty checks
if (string === "") {}
if (array.length === 0) {}
if (items.length > 0) {}

// Explicit number checks
if (count === 0) {}
if (index !== -1) {}

// Boolean values CAN use implicit checks
if (!isActive) {}
if (user.isVerified) {}
Why this matters:
  • 0, "", false, null, undefined are all falsy in JavaScript
  • Implicit checks cause bugs when these are valid values
  • Explicit checks document intent clearly

Single Props Object Pattern (React)

Never destructure props in function signatures. Use props.propertyName for better refactoring.
type UserProfileProps = {
  userId: string;
  showActions: boolean;
};

function UserProfile(props: UserProfileProps) {
  return (
    <div>
      <h1>User: {props.userId}</h1>
      {props.showActions && <button>Edit</button>}
    </div>
  );
}
Exception: Shadcn UI components (generated code) may use destructuring.

Backend Object Pattern

Use the centralized backend object for all API calls. Never import backend functions directly.
import { backend } from "@/modules/backend/backend.ts";

const profile = await backend.getProfile("en", id);
const stories = await backend.getStoriesByKinds(locale, ["article"]);

CSS Modules with @apply

Use CSS Modules with Tailwind’s @apply directive as the primary styling approach.
.card {
  @apply border rounded-lg p-4 shadow-md;
  @apply flex flex-col gap-2;
}

.card .title {
  @apply text-xl font-bold mb-2;
}

.card .description {
  @apply text-sm text-gray-600;
}
When direct Tailwind is acceptable:
  • Very simple micro-adjustments
  • Global layout containers
  • Use cn() utility when combining: className={cn(styles.button, "mt-2")}

Pure Function Extraction for Testability

Separate pure logic from environment dependencies to enable Deno testing.
export const SUPPORTED_LOCALES = ["en", "tr", "fr"] as const;

export function isValidLocale(locale: string): boolean {
  return SUPPORTED_LOCALES.includes(locale as any);
}

export function getDefaultLocale(): string {
  return "en";
}
Naming convention: foo-utils.ts for pure functions, foo.ts for config-aware wrappers.

TypeScript/JavaScript (Frontend)

Module Patterns

Use named exports (no default exports except for framework-required files):
export function buildCommand() {}
export const CONFIG_PATH = "./config";
export class UserService {}
export type BuildOptions = {};
Prefer namespace imports to prevent collisions:
import * as path from "@std/path";
import * as fs from "@std/fs";

const filePath = path.join(dir, "config.ts");
const exists = await fs.exists(filePath);

Syntax Rules

Use const by default, let only when reassignment needed:
const userName = "John";        // ✅
const config = { port: 8000 };  // ✅
let counter = 0;                // ✅ (will be reassigned)
counter++;

let userName = "John";          // ❌ Never reassigned
Always use strict equality (===) and nullish coalescing (??):
// ✅ Strict equality
if (value === 0) {}
if (user === null) {}

// ✅ Nullish coalescing (only null/undefined)
const port = config.port ?? 8000;
const name = user.name ?? "Guest";

// ❌ Loose equality
if (value == 0) {}  // matches 0, "0", false, ""

// ❌ Logical OR (fails for 0, "")
const port = config.port || 8000;  // Wrong if port is 0
Use template literals and slice():
// ✅ Template literals
const greeting = `Hello, ${user.name}!`;
const url = `/api/users/${userId}/posts`;

// ✅ slice() for substrings
const first5 = text.slice(0, 5);
const last5 = text.slice(-5);

// ❌ String concatenation
const greeting = "Hello, " + user.name + "!";

// ❌ substring() or substr() (deprecated)
const first5 = text.substring(0, 5);
Use return await consistently for better stack traces:
// ✅ Explicit await
async function fetchUser(id: string): Promise<User> {
  return await userRepository.findById(id);
}

// ✅ Critical for try-catch (without await, rejection bypasses catch)
async function processData(): Promise<Result> {
  try {
    return await riskyOperation();
  } catch (error) {
    return await fallbackOperation();
  }
}

// ❌ Implicit return (worse stack traces, breaks try-catch)
async function fetchUser(id: string): Promise<User> {
  return userRepository.findById(id);
}

React Patterns

React v19 Compiler Compatibility - Let the compiler handle optimization:
// ✅ Let compiler optimize
function ExpensiveList(props: { items: Item[] }) {
  const sorted = props.items.toSorted((a, b) => a.name.localeCompare(b.name));
  return (
    <ul>
      {sorted.map((item) => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

// ❌ Unnecessary manual memoization
function ExpensiveList(props: { items: Item[] }) {
  const sorted = useMemo(
    () => props.items.toSorted((a, b) => a.name.localeCompare(b.name)),
    [props.items],
  );
  return <ul>{sorted.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}
Only use useMemo/useCallback after profiling shows measurable benefit.

Translation System

Use English text as translation keys:
// ✅ English as key
const title = t("Home", "Welcome to Aya");
const button = t("Auth", "Login with GitHub");

// ✅ With fallback
const label = t("Section", "Key") || "Default Text";

// ❌ Snake case keys
const title = t("Home.welcome_title");
Server vs Client Components:
  • Server Components: getTranslations()
  • Client Components: useTranslations() hook

Go (Backend)

Hexagonal Architecture

Strict separation of concerns:
apps/services/
├── cmd/                    # Application entrypoints
│   ├── serve/              # HTTP server
│   └── cli/                # CLI commands
└── pkg/
    ├── ajan/               # Shared framework
    └── api/
        ├── business/       # Pure business logic (NO external deps)
        └── adapters/       # External integrations (HTTP, DB, Redis)
Business logic rules:
  • NO external dependencies (no HTTP, database, Redis imports)
  • Define interfaces (ports) within business layer
  • Depend only on other business logic or interfaces
  • All composition happens in pkg/api/adapters/appcontext/
// pkg/api/business/user/service.go
package user

type Repository interface {  // Port defined in business layer
    FindByID(ctx context.Context, id string) (*User, error)
}

type Service struct {
    repo Repository  // Interface, not concrete type
}

func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

File and Code Style

File naming: Use snake_case for all Go files:
✅ user_service.go
✅ payment_handler.go
✅ auth_middleware.go

❌ UserService.go
❌ paymentHandler.go
Error handling: Always check and wrap errors with context:
// ✅ Wrapped errors with sentinel
var ErrUserNotFound = errors.New("user not found")
var ErrInvalidInput = errors.New("invalid input")

result, err := operation()
if err != nil {
    return fmt.Errorf("%w: %w", ErrOperationFailed, err)
}

// ❌ Ignored or unwrapped errors
result, _ := operation()  // Ignored

if err != nil {
    return err  // No context
}

if err != nil {
    return errors.New("failed")  // Original error lost
}
JSON encoding: Use encoding/json/v2 (jsonv2):
import "encoding/json/v2"  // ✅ Use v2

type User struct {
    Name  string `json:"name"`
    Email string `json:"email,omitzero"`  // ✅ v2 tag
}

data, err := json.Marshal(user)

Logging Conventions

Use appropriate log levels by layer:
// Service layer - info for successful operations
log.Info("user created", "userId", user.ID, "traceId", traceID)

// Repository layer - debug only
log.Debug("fetching user from database", "userId", id)

// Error with full context before propagating
log.Error("failed to create user", "userId", id, "error", err)
Never log sensitive information (passwords, tokens, API keys).

Code Quality Principles

Self-Documenting Code

Use meaningful names that explain intent:
function calculateTotalPrice(items: Item[], taxRate: number): number {
  const subtotal = items.reduce((sum, item) => sum + item.price, 0);
  const tax = subtotal * taxRate;
  return subtotal + tax;
}

class UserAuthenticationService {
  async validateCredentials(username: string, password: string): Promise<boolean> {
    // ...
  }
}

Comments

Explain “why” and “how”, not “what” (code shows “what”):
// ✅ Explains reasoning
// Use binary search because dataset can exceed 10M items
// Linear search would be O(n), binary search is O(log n)
function findUser(users: User[], id: string): User | null {
  // Binary search implementation
}

// ✅ Documents trade-offs
// Cache results for 5 minutes to reduce database load
// Trade-off: slight staleness for better performance
const cache = new Map<string, CachedValue>();

// ❌ Redundant comment
// This function finds a user
function findUser(users: User[], id: string): User | null {}

// ❌ No explanation
const x = 5 * 60 * 1000;  // Why this value?

Avoid Magic Values

Use named constants instead of magic numbers/strings:
// ✅ Named constants
const MAX_RETRIES = 3;
const API_TIMEOUT_MS = 5000;
const DEFAULT_PAGE_SIZE = 20;

if (retries >= MAX_RETRIES) {}
setTimeout(callback, API_TIMEOUT_MS);

// ❌ Magic values
if (retries >= 3) {}  // What is 3?
setTimeout(callback, 5000);  // What is 5000?

Formatting and Linting

Frontend (Deno)

# Format code
deno fmt

# Check formatting
deno fmt --check

# Lint code
deno lint

# Line width: 120 characters (configured in deno.json)

Backend (Go)

# Format code
make fix  # Runs go fmt, betteralign, modernize

# Lint code
make lint  # Runs golangci-lint

# Static analysis
make check  # Runs govulncheck, betteralign, vet

Root Quality Check

Always run before committing:
make ok  # Runs ALL checks (backend + frontend)
This is enforced by pre-commit hooks.

Project-Specific Rules

Internationalization (13 Locales)

Supported locales: ar, de, en, es, fr, it, ja, ko, nl, pt-PT, ru, tr, zh-CN Rules:
  • NEVER put English text in non-English locale files
  • Every translation key must exist in ALL 13 locales
  • Use 3-tier fallback (requested → entity default → any available)
  • Always strings.TrimRight(value, " ") for _tx table locale codes (CHAR(12) padding)

Accessibility

// ✅ Use data-slot for CSS hooks (not invalid ARIA roles)
<a data-slot="card" href="...">

// ✅ aria-label for icon-only buttons
<Button aria-label="Close menu">
  <XIcon />
</Button>

// ❌ Invalid ARIA role
<a role="card" href="...">  // "card" is not a valid ARIA role

Common Mistakes

Most common violations:
  1. Using truthy/falsy checks instead of explicit comparisons
  2. Destructuring props in React components
  3. Importing backend functions directly instead of using backend object
  4. Using inline Tailwind instead of CSS Modules
  5. Mixing pure logic with environment dependencies
  6. Ignoring or not wrapping errors
  7. Using let when const would work

Further Reading

Build docs developers (and LLMs) love