Files
SimpleFinanceDash/.planning/codebase/CONVENTIONS.md

9.2 KiB

Coding Conventions

Analysis Date: 2026-03-11

Naming Patterns

Files:

  • React components: PascalCase (e.g., AppLayout.tsx, LoginPage.tsx)
  • TypeScript utilities/libraries: camelCase (e.g., useAuth.ts, api.ts, utils.ts)
  • Go packages: lowercase, no underscore (e.g., auth, db, models, api)
  • Go files: descriptive lowercase (e.g., handlers.go, queries.go, auth.go)
  • Test files: None currently present (testing infrastructure not yet established)

Functions:

  • React hooks: use prefix in camelCase (e.g., useAuth(), useBudgets())
  • Go receiver methods: ExportedName style on receiver (e.g., (h *Handlers) Register)
  • Go internal functions: camelCase for unexported, PascalCase for exported (e.g., writeJSON(), ListCategories())
  • TypeScript functions: camelCase (e.g., request<T>(), cn())

Variables:

  • Local variables: camelCase (e.g., userID, displayName, carryover)
  • Constants: UPPER_SNAKE_CASE in Go (e.g., CategoryBill, userIDKey), camelCase in TypeScript (e.g., API_BASE)
  • React state: camelCase for state and setter (e.g., [user, setUser], [loading, setLoading])

Types:

  • TypeScript interfaces: PascalCase, no I prefix (e.g., User, Category, Budget, BudgetItem)
  • TypeScript type aliases: PascalCase (e.g., CategoryType = 'bill' | 'variable_expense' | ...)
  • Go structs: PascalCase (e.g., User, Category, Budget)
  • Go type enums: PascalCase with prefixed constants (e.g., CategoryType, CategoryBill, CategoryIncome)

Code Style

Formatting:

  • TypeScript: Vite default (2-space indentation implied by no explicit prettier config)
  • Go: gofmt standard (1-tab indentation)
  • Language-specific linters enforced via npm run lint (ESLint) and go vet

Linting:

  • Frontend: ESLint with TypeScript support (eslint.config.js)
    • Extends: @eslint/js, typescript-eslint, eslint-plugin-react-hooks, eslint-plugin-react-refresh
    • Targets: All .ts and .tsx files
    • Rules enforced: React hooks rules, React refresh rules
  • Go: Standard go vet (no additional linter configured)

TypeScript Compiler Settings:

  • strict: true - Full strict type checking enabled
  • noUnusedLocals: true - Unused variables are errors
  • noUnusedParameters: true - Unused parameters are errors
  • noFallthroughCasesInSwitch: true - Switch cases must explicitly break or return
  • noUncheckedSideEffectImports: true - Side-effect imports must be explicit
  • Target: ES2022, Module: ESNext

Import Organization

TypeScript Order:

  1. Third-party React/UI imports (e.g., from 'react', from 'react-router-dom')
  2. Component/utility imports from @/ alias
  3. Type imports (e.g., type User)
  4. CSS/global imports (e.g., '@/i18n')

Example from src/components/AppLayout.tsx:

import { useTranslation } from 'react-i18next'
import { Link, useLocation } from 'react-router-dom'
import { LayoutDashboard, Tags, Settings, LogOut } from 'lucide-react'
import { Sidebar, ... } from '@/components/ui/sidebar'
import { Button } from '@/components/ui/button'
import type { AuthContext } from '@/hooks/useAuth'

Go Import Order:

  1. Standard library imports (e.g., "context", "encoding/json")
  2. Third-party imports (e.g., "github.com/go-chi/chi/v5")
  3. Internal imports (e.g., "simplefinancedash/backend/internal/...")

Example from backend/internal/api/handlers.go:

import (
	"encoding/json"
	"net/http"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"simplefinancedash/backend/internal/auth"
)

Path Aliases:

  • TypeScript: @/* maps to ./src/* (defined in tsconfig.json)
  • Go: Full module paths from module name simplefinancedash/backend

Error Handling

TypeScript Patterns:

  • Silent error suppression with fallback (preferred for non-critical operations):
    try {
      const u = await auth.me()
      setUser(u)
    } catch {
      setUser(null)  // Fallback without logging
    }
    
  • Custom error class for API errors:
    export class ApiError extends Error {
      status: number
      constructor(status: number, message: string) {
        super(message)
        this.name = 'ApiError'
        this.status = status
      }
    }
    
  • HTTP response error handling in request<T>():
    if (!res.ok) {
      const body = await res.json().catch(() => ({}))
      throw new ApiError(res.status, body.error || res.statusText)
    }
    

Go Patterns:

  • Wrap errors with context using fmt.Errorf("context: %w", err) for traceability:
    hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
      return "", fmt.Errorf("hashing password: %w", err)
    }
    
  • Return early on error in handlers:
    if err != nil {
      writeError(w, http.StatusBadRequest, "invalid request body")
      return
    }
    
  • Silent errors in non-critical flows with zero-value fallback:
    var exists bool
    err := pool.QueryRow(...).Scan(&exists)
    if err != nil {
      exists = false  // Assume false on error
    }
    

Logging

Framework: No structured logging library. Uses log package in Go, console in TypeScript.

Patterns:

  • Go: log.Printf() for informational messages (e.g., "Server starting on :8080"), log.Fatalf() for fatal errors during startup
    • Located in backend/cmd/server/main.go
  • TypeScript: No logging in production code. Errors silently caught and handled with UI fallbacks.

Comments

When to Comment:

  • Go: Comments for exported types/functions (standard Go convention)
    • Comment each exported function above its definition
    • Used in backend/internal/auth/auth.go for public auth functions
  • TypeScript: Minimal comments; code clarity preferred
    • JSDoc comments not currently used
    • Comments only for non-obvious logic or integration details

Current Usage:

  • Go handler comments: "// Auth Handlers", "// Helpers" as section dividers in api/handlers.go
  • TypeScript code: Inline comments absent (code is self-documenting with clear naming)

Function Design

Size:

  • Go handler functions: 10-50 lines (moderate size with early returns for validation)
  • Go query functions: Variable (from 5 lines for simple queries to 30+ for complex operations like GetBudgetWithItems())
  • TypeScript hooks: 20-40 lines per hook (following React patterns with useState/useEffect)

Parameters:

  • Go: Receiver pattern for struct methods, context as first parameter in database functions
    • Example: func (q *Queries) CreateUser(ctx context.Context, email, passwordHash, displayName, locale string) (*models.User, error)
  • TypeScript: Props as single typed object, destructured in function signature
    • Example: export function AppLayout({ auth, children }: Props)
  • React hooks: No parameters passed to hooks; they use internal state

Return Values:

  • Go: Multiple returns with error as last return (e.g., (*models.User, error))
  • TypeScript: Single return object containing multiple pieces of state/functions
    • Example: useBudgets() returns { list, current, loading, fetchList, selectBudget, setCurrent }

Module Design

Exports:

  • Go: Exported types/functions start with capital letter (PascalCase)
    • Non-exported functions use camelCase
    • Pattern: Define helpers after main exports in same file
  • TypeScript: Named exports for components/utilities, default exports for page components
    • Example page export: export default function App()
    • Example utility export: export class ApiError extends Error
    • Example hook export: export function useAuth()

Barrel Files:

  • No barrel files currently used (no index.ts re-exports in src/lib, src/hooks, etc.)
  • Each utility/hook imported directly from its file
  • Components from shadcn/ui imported from @/components/ui/[component-name]

API Client Pattern

Location: frontend/src/lib/api.ts

Structure:

// 1. Helper function for all requests
async function request<T>(path: string, options?: RequestInit): Promise<T> { ... }

// 2. Custom error class
export class ApiError extends Error { ... }

// 3. Type definitions
export interface User { ... }
export interface Category { ... }

// 4. Namespace objects for endpoint groups
export const auth = {
  register: (...) => request<User>(...),
  login: (...) => request<User>(...),
  ...
}

export const categories = {
  list: () => request<Category[]>(...),
  create: (...) => request<Category>(...),
  ...
}

This pattern provides:

  • Type safety with generic request<T>
  • Automatic error handling and JSON parsing
  • Organized endpoint grouping by resource
  • Consistent authentication (cookies via credentials: 'include')

React Component Pattern

Props Pattern:

  • Define interface above component: interface Props { ... }
  • Destructure in function signature: export function Component({ prop1, prop2 }: Props)
  • Type children explicitly: children: React.ReactNode

Example from AppLayout.tsx:

interface Props {
  auth: AuthContext
  children: React.ReactNode
}

export function AppLayout({ auth, children }: Props) { ... }

Hook Usage:

  • Always call hooks at top level of component
  • Use useCallback for memoized functions passed to child components
  • Combine related state with custom hooks (e.g., useAuth(), useBudgets())

Convention analysis: 2026-03-11