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.
product-card.module.css
ProductCard.tsx
.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")}
Separate pure logic from environment dependencies to enable Deno testing.
locale-utils.ts (Pure)
config.ts (Wrapper)
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/
Correct - Business Layer
Incorrect - Business Layer with External Deps
// 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 > {
// ...
}
}
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?
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:
Using truthy/falsy checks instead of explicit comparisons
Destructuring props in React components
Importing backend functions directly instead of using backend object
Using inline Tailwind instead of CSS Modules
Mixing pure logic with environment dependencies
Ignoring or not wrapping errors
Using let when const would work
Further Reading