Architecture Review
Quick Reference — Top 15 Architecture Red Flags
- Business logic in components — fetching, transforming, validating all in one component
- God component — one component >300 lines doing everything
- API route doing too much — should delegate to service layer
- DB queries scattered — no data access layer, queries in components and routes
-
'use client'on layout — forces entire subtree client-side - Prop drilling >3 levels — should use composition, context, or state management
- Multiple sources of truth — same data duplicated in different stores
- Missing loading/error states — no loading.tsx, error.tsx, or not-found.tsx
- No input validation on API routes — missing Zod schemas
- No pagination on list endpoints — returns unbounded results
- Inconsistent error response shapes — different formats across routes
- Barrel exports killing tree-shaking — index.ts re-exporting everything
- Circular dependencies — module A imports B imports A
- Global state for local concerns — form state in global store
- Missing Server Action validation — trusting client-side input
Separation of Concerns
The Layered Architecture (for our stack)
┌─────────────────────────┐
│ Components (UI only) │ React components render UI, handle user interaction
│ No business logic │ Import hooks, not services directly
├─────────────────────────┤
│ Hooks / Actions │ Custom hooks for client state, Server Actions for mutations
│ Orchestrate calls │ Call services, handle loading/error states
├─────────────────────────┤
│ Services / Lib │ Business logic, validation, transformation
│ Framework-agnostic │ Testable without React, reusable across routes
├─────────────────────────┤
│ Data Access (Supabase)│ All DB queries centralized here
│ Type-safe queries │ Generated types, consistent error handling
└─────────────────────────┘
God Component Anti-Pattern
// ❌ Everything in one component
export function Dashboard() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [filter, setFilter] = useState('')
const [sort, setSort] = useState('name')
// 20 more state variables...
useEffect(() => {
// 30 lines of data fetching
}, [filter, sort])
function handleDelete(id) {
// 15 lines of delete logic with confirmation
}
function handleExport() {
// 20 lines of CSV export logic
}
// 200 more lines of JSX
}
// ✅ Decomposed
export function Dashboard() {
return (
<DashboardProvider>
<DashboardFilters />
<UserTable />
<ExportButton />
</DashboardProvider>
)
}
Next.js App Router Architecture
Server Actions: Security + Architecture
// ❌ No validation, no auth, business logic inline
'use server'
export async function updateProfile(formData: FormData) {
const name = formData.get('name')
await supabase.from('profiles').update({ name }).eq('id', '???')
}
// ✅ Validated, authenticated, delegated
'use server'
import { z } from 'zod'
import { createClient } from '@/lib/supabase/server'
import { updateUserProfile } from '@/lib/services/profile'
const UpdateProfileSchema = z.object({
name: z.string().min(1).max(100).trim(),
})
export async function updateProfile(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Unauthorized' }
const parsed = UpdateProfileSchema.safeParse({ name: formData.get('name') })
if (!parsed.success) return { error: 'Invalid input', details: parsed.error.flatten() }
return updateUserProfile(supabase, user.id, parsed.data)
}
API Routes vs Server Actions
| Use Case | Use API Route | Use Server Action |
|---|---|---|
| Form submission from own UI | ✅ | |
| Third-party webhook receiver | ✅ | |
| Public API for external consumers | ✅ | |
| File upload with progress | ✅ | |
| Simple mutation (create/update/delete) | ✅ | |
| CORS-required endpoint | ✅ |
Data Fetching Hierarchy
layout.tsx → Fetch data shared across all child pages (user session, nav data)
page.tsx → Fetch data specific to this page (page content, list data)
component → Only fetch in client components for real-time/interactive data
Rule: Fetch as high as possible, as specific as needed. Don't fetch in layouts what only one page needs.
Loading/Error/Not-Found Pattern
Every route segment should consider:
app/
dashboard/
page.tsx # Main content
loading.tsx # Shown while page.tsx fetches (Suspense boundary)
error.tsx # Shown if page.tsx throws ('use client' required)
not-found.tsx # Shown when notFound() is called
Flag missing: If a page does async data fetching but has no loading.tsx or error.tsx.
State Management Red Flags
| Red Flag | Better Approach |
|---|---|
| Form state in Zustand/Redux | useState in the form component |
| Server data in global store | SWR/TanStack Query with cache |
| UI state (modal open) in global store | useState or URL params |
| Derived data stored separately | Compute from source (useMemo) |
| Same data in multiple stores | Single source of truth + selectors |
API Design Review
Consistent Error Responses
// ❌ Inconsistent — different shapes across routes
// Route A: { message: 'Not found' }
// Route B: { error: 'Invalid input' }
// Route C: { errors: [{ field: 'email', msg: 'required' }] }
// ✅ Consistent shape everywhere
type ApiError = {
error: {
code: string // machine-readable: 'NOT_FOUND', 'VALIDATION_ERROR'
message: string // human-readable
details?: unknown // optional: field errors, context
}
}
// 404: { error: { code: 'NOT_FOUND', message: 'User not found' } }
// 400: { error: { code: 'VALIDATION_ERROR', message: 'Invalid input', details: {...} } }
// 500: { error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } }
File Structure: Feature-Based
// ❌ Layer-based (hard to navigate at scale)
src/
components/
UserCard.tsx
OrderList.tsx
ProductGrid.tsx
hooks/
useUser.ts
useOrders.ts
services/
userService.ts
orderService.ts
// ✅ Feature-based (everything related is together)
src/
features/
users/
components/UserCard.tsx
hooks/useUser.ts
services/userService.ts
types.ts
orders/
components/OrderList.tsx
hooks/useOrders.ts
actions/createOrder.ts
shared/
components/Button.tsx
lib/supabase.ts
Sources
- Next.js App Router documentation
- React Server Components architecture
- Clean Architecture (Robert C. Martin)
Changelog
- 2026-03-21: Initial skill — architecture review for Next.js + Supabase