| name | coding-patterns |
| description | Use this when writing or modifying code in FOSSAPP. Provides Next.js App Router patterns, server action organization, dual Supabase client rules, TypeScript conventions, validation patterns, error handling, and naming conventions. |
FOSSAPP Coding Patterns
Essential patterns and conventions for writing code in FOSSAPP. Follow these patterns for all new features and modifications.
Module Splitting (MANDATORY)
Large files MUST be split into focused, single-responsibility modules.
Thresholds
| File Size | Action |
|---|---|
| > 500 lines | Consider splitting |
| > 800 lines | MUST split |
Patterns
Server Actions → Create subdirectory with focused modules:
src/lib/actions/
├── projects.ts # Re-export (backward compat)
└── projects/ # Focused modules
├── index.ts # Barrel export (NO 'use server')
├── project-crud-actions.ts # Has 'use server'
└── project-product-actions.ts
Page Components → Co-locate with page.tsx:
src/app/projects/[id]/
├── page.tsx # Main (reduced to ~500 lines)
└── components/
├── index.ts # Barrel export
├── project-overview-tab.tsx
└── utils.tsx
Complex Components → Extract sub-components:
src/components/planner/
├── planner-viewer.tsx # Main component
├── viewer-toolbar.tsx # Extracted
└── viewer-overlays.tsx # Extracted
Key Rules
- Barrel exports (
index.ts) must NOT have'use server' - Each action file has
'use server'at top - Original files become re-exports for backward compatibility
- Use descriptive names:
*-crud-actions.ts,*-tab.tsx
Full details: .claude/monorepo-development-guidelines.md
Server Actions (Domain Organization)
Structure
Organize server actions by business domain, not operation type:
src/lib/actions/
├── index.ts # Re-exports all actions
├── validation.ts # Shared validation utilities
├── dashboard.ts # Stats, analytics, aggregations
├── customers.ts # Customer CRUD
├── products.ts # Product search, details
├── projects.ts # Project management
└── taxonomy.ts # Category tree operations
Pattern for Domain Files
// src/lib/actions/[domain].ts
'use server'
import { supabaseServer } from '../supabase-server'
import { validateXxx } from './validation'
import { PAGINATION } from '@/lib/constants'
// ============================================================================
// INTERFACES
// ============================================================================
export interface DomainItem {
id: string
name: string
}
export interface DomainListParams {
page?: number
pageSize?: number
sortBy?: string
sortOrder?: 'asc' | 'desc'
}
// ============================================================================
// SEARCH
// ============================================================================
export async function searchDomainAction(query: string): Promise<DomainItem[]> {
try {
const sanitizedQuery = validateSearchQuery(query)
const { data, error } = await supabaseServer
.schema('schema_name')
.from('table_name')
.select('...')
.ilike('name', `%${sanitizedQuery}%`)
.limit(PAGINATION.DEFAULT_SEARCH_LIMIT)
if (error) {
console.error('Domain search error:', error)
return []
}
return data || []
} catch (error) {
console.error('Search domain error:', error)
return []
}
}
Importing Actions
// Preferred: Import from domain file directly
import { searchCustomersAction } from '@/lib/actions/customers'
// Also valid: Import from index (backward compatible)
import { searchCustomersAction } from '@/lib/actions'
Dual Supabase Client Pattern ⚠️ CRITICAL
NEVER mix these up! Using the wrong client is a security vulnerability.
Server-Side (Actions & API Routes)
import { supabaseServer } from '@/lib/supabase-server'
export async function serverAction() {
const { data, error } = await supabaseServer
.from('items.product_info')
.select('*')
return data
}
Uses: SUPABASE_SERVICE_ROLE_KEY (full admin access)
Never expose to client!
Client-Side (Components)
import { supabase } from '@/lib/supabase'
export function ClientComponent() {
const fetchData = async () => {
const { data } = await supabase
.from('items.product_info')
.select('*')
return data
}
}
Uses: NEXT_PUBLIC_SUPABASE_ANON_KEY (limited permissions)
Rule of Thumb
- Server actions →
supabaseServer(service role) - API routes →
supabaseServer(service role) - Client components →
supabase(anon key) - When in doubt → Use server action with
supabaseServer
Server vs Client Components
Default: Server Components
// No 'use client' directive = Server Component
export default function ProductPage() {
return <div>Server-rendered content</div>
}
Benefits:
- Better performance (less JavaScript)
- SEO-friendly
- Direct database access
- Automatic code splitting
Client Components
'use client'
import { useState } from 'react'
export default function InteractiveComponent() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
Use when you need:
- React hooks (useState, useEffect, useContext)
- Browser APIs (localStorage, window)
- Event handlers (onClick, onChange)
- Third-party libraries requiring client context
Composition Pattern
Keep client components small and compose them in server components:
// app/page.tsx (Server Component)
import { InteractiveButton } from '@/components/interactive-button'
export default function Page() {
return (
<div>
<h1>Server-rendered heading</h1>
<InteractiveButton /> {/* Client component */}
</div>
)
}
Validation Patterns
Centralized Validation
All validation in src/lib/actions/validation.ts:
'use server'
import { VALIDATION } from '@/lib/constants'
export function validateSearchQuery(query: string): string {
if (!query || typeof query !== 'string') {
throw new Error('Invalid search query')
}
const sanitized = query.trim().slice(0, VALIDATION.SEARCH_QUERY_MAX_LENGTH)
if (sanitized.length === 0) {
throw new Error('Search query cannot be empty')
}
return sanitized
}
export function validateUUID(id: string, fieldName: string): string {
if (!id || !VALIDATION.UUID_REGEX.test(id)) {
throw new Error(`Invalid ${fieldName} format`)
}
return id
}
Usage in Actions
import { validateSearchQuery, validateUUID } from './validation'
export async function getProductByIdAction(productId: string) {
const sanitizedId = validateUUID(productId, 'product ID')
// ...
}
Constants & Configuration
All magic numbers and configuration values go in src/lib/constants.ts:
export const VALIDATION = {
SEARCH_QUERY_MAX_LENGTH: 100,
UUID_REGEX: /^[0-9a-f]{8}-...-[0-9a-f]{12}$/i,
} as const
export const PAGINATION = {
DEFAULT_SEARCH_LIMIT: 50,
DEFAULT_PAGE_SIZE: 20,
MAX_BATCH_SIZE: 1000,
} as const
export const DASHBOARD = {
TOP_FAMILIES_LIMIT: 10,
ACTIVE_USERS_LIMIT: 5,
} as const
Usage:
import { PAGINATION } from '@/lib/constants'
.limit(PAGINATION.DEFAULT_SEARCH_LIMIT) // ✅ Good
.limit(50) // ❌ Bad (magic number)
Error Handling
Standard Pattern
export async function someAction(): Promise<ResultType> {
try {
const { data, error } = await supabaseServer.from('table').select('*')
if (error) {
// Log with context, no sensitive data
console.error('Action name error:', {
message: error.message,
code: error.code
})
return defaultValue // Empty array, null, or error object
}
return data || defaultValue
} catch (error) {
// Catch unexpected errors
console.error('Action name exception:',
error instanceof Error ? error.message : 'Unknown error'
)
return defaultValue
}
}
Return Patterns
| Scenario | Return Value |
|---|---|
| List/Search failed | [] (empty array) |
| Get by ID failed | null |
| Stats failed | Object with zero values |
| Paginated list failed | Full result object with empty items |
Naming Conventions
Actions
[verb][Domain]Action
searchProductsAction
getCustomerByIdAction
listProjectsAction
getDashboardStatsAction
Database Functions
schema.verb_noun_with_params
items.get_dashboard_stats
items.get_supplier_stats
search.search_products_with_filters
Types
[Domain][Purpose]
ProductSearchResult # For search/list results
ProductDetail # For full detail view
CustomerListParams # For list operation params
Files
Domain files: lowercase, singular (customers.ts, not customer.ts)
Type files: lowercase, singular (product.ts)
Component files: PascalCase (FilterPanel.tsx)
shadcn/ui Component Patterns
Adding Components
npx shadcn@latest add dialog
npx shadcn@latest add table
Usage Pattern
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export function MyComponent() {
return (
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>
<Button variant="default">Click me</Button>
</CardContent>
</Card>
)
}
FossSpinner (Custom Branded Spinner)
import { Spinner } from '@/components/ui/spinner'
// Standard usage
<Spinner size="lg" /> // sm=20, md=32, lg=48, xl=64
// Dark backgrounds
<Spinner size="lg" variant="dark" /> // White F for dark backgrounds
// Standard placement
<div className="flex items-center justify-center flex-1">
<Spinner size="lg" />
</div>
Design Guidelines:
- Always center spinners in main content area, not sidebars
- Use
variant="dark"on dark backgrounds (e.g.,bg-black) - Use
variant="auto"(default) for responsive dark mode
Type Definitions
Location
src/types/
├── product.ts # ProductInfo, ProductSearchResult, Feature
├── search.ts # SearchFilters, FilterDefinition, Facets
├── customer.ts # CustomerDetail, CustomerSearchResult
└── index.ts # Re-exports (optional)
Pattern
// src/types/domain.ts
/**
* Lightweight type for list/search results
*/
export interface DomainSearchResult {
id: string
name: string
}
/**
* Full type for detail views
*/
export interface DomainDetail extends DomainSearchResult {
description: string
created_at: string
}
/**
* Params for list operations
*/
export interface DomainListParams {
page?: number
pageSize?: number
sortBy?: 'name' | 'created_at'
sortOrder?: 'asc' | 'desc'
}
Database Query Patterns
PostgreSQL Functions (RPC)
Use PostgreSQL functions when:
- Aggregation needed - COUNT, SUM, GROUP BY operations
- Multiple related queries - Batch into single call
- Complex joins - Let PostgreSQL optimize
- Performance critical - Avoid transferring large datasets
const { data, error } = await supabaseServer
.schema('schema_name')
.rpc('function_name', {
p_param1: value,
p_limit: 10
})
if (error) {
console.error('RPC error:', error)
return defaultValue
}
return (data || []).map((item: DBResponseType) => ({
field1: item.column1,
field2: Number(item.column2) // bigint → number
}))
Schema Organization
items.* → Product/catalog functions
analytics.* → User tracking, metrics
search.* → Search/filter functions
customers.* → Customer functions
projects.* → Project functions
Security Checklist
Always:
- ✅ Validate all inputs (regex, trim, length)
- ✅ Use parameterized queries (Supabase client)
- ✅ Limit result sets (
.limit(50)) - ✅ Sanitize user input before database queries
- ✅ Return generic error messages (don't expose internals)
- ✅ Use
supabaseServerfor server-side operations - ✅ Use
supabasefor client-side operations
Never:
- ❌ Trust user input directly
- ❌ Concatenate SQL strings manually
- ❌ Return raw database errors to client
- ❌ Expose service role key to client
- ❌ Skip input validation
- ❌ Mix up supabase clients
Quick Reference
Adding New Features Checklist
- Types first: Define interfaces in
src/types/[domain].ts - Database functions: If aggregation needed, create migration
- Validation: Add validators to
src/lib/actions/validation.ts - Action file: Create
src/lib/actions/[domain].ts - Export: Add to
src/lib/actions/index.ts - Constants: Add any magic numbers to
src/lib/constants.ts - Tests: Manual testing via dev server
Common Patterns
Loading States:
const [isLoading, setIsLoading] = useState(false)
const handleAction = async () => {
setIsLoading(true)
try {
await fetchData()
} finally {
setIsLoading(false)
}
}
if (isLoading) return <Spinner />
Data Fetching:
'use client'
import { useState } from 'react'
import { searchProductsAction } from '@/lib/actions'
export function ProductSearch() {
const [results, setResults] = useState([])
const handleSearch = async (query: string) => {
try {
const data = await searchProductsAction(query)
setResults(data)
} catch (error) {
console.error(error)
}
}
return <Results data={results} />
}
See Also
- Full architecture docs: docs/architecture/
- API patterns: docs/architecture/api-patterns.md
- Component guide: docs/architecture/components.md