| name | frontend-architecture |
| description | How to organize frontend code — separation of concerns (UI / logic / data / type), file responsibility, state tiers, API services, schema validation, and framework conventions for React/Next and Vue. Structural rules, not visual design. |
| when_to_use | When structuring a frontend codebase or reviewing how frontend code is organized — where logic, API calls, state, types, and validation should live; component vs hook/composable boundaries; Next.js server/client split; Vue Composition API. NOT for visual design (use frontend-design) and NOT for React/Next performance rules (use nextjs-react-expert). |
| allowed-tools | Read, Write, Edit, Glob, Grep |
Frontend Architecture
How to organize frontend code so it scales. Separation of concerns over file-type folders.
Applies to React/Next and Vue. For directory layout, follow app-builder. For visual design, see frontend-design. For React/Next performance, see nextjs-react-expert.
1. Separation of Concerns — the core rule
Split code into four layers by responsibility. A unit of code does ONE of these, not several:
| Layer | Holds | Lives in |
|---|
| UI | Rendering, markup, presentational state | components/ |
| Logic | State, effects, data transforms, reusable UI logic | hooks/ (React) · composables/ (Vue) |
| Data | API calls, fetch/axios, cache keys | lib/ / service files (*.api.ts) |
| Type | TypeScript types, domain models | types.ts / *.types.ts |
| Validation | Form/data schemas | *.schema.ts (zod/yup/valibot) |
Directory layout (top-level folders) follows the project's scaffolding skill — do not invent a competing structure here. This skill is about which layer code belongs to, not where the folders sit.
2. File Responsibility & Size
One clear responsibility per file. Size is a signal, not a hard limit — a clear 230-line file beats a 90-line file that fetches, validates, renders, and juggles modals + toasts.
| File type | Comfortable range |
|---|
| UI component | 80–180 lines |
| Page / screen | 100–220 lines |
| Hook / composable | 40–150 lines |
| API service | 50–200 lines |
| Type / schema | flexible |
Split a file when it mixes UI + API + business logic + validation + state. Don't split a coherent file just to hit a number.
3. Components render UI; logic goes elsewhere
Components should primarily render. Push fetching/state/transforms into a hook or composable.
function ProductList() {
const [products, setProducts] = useState([])
useEffect(() => { fetch('/api/products').then(r => r.json()).then(setProducts) }, [])
return <div>{/* render */}</div>
}
function ProductList() {
const { products, isLoading } = useProducts()
if (isLoading) return <Loading />
return <div>{/* render */}</div>
}
- Custom hooks must start with
use.
- A component calling an API directly is acceptable only for the smallest one-off cases.
4. Next.js — Server Components by default
In the App Router, page.tsx and layout.tsx are Server Components. Reach for "use client" only when you actually need the client.
| Server Component | Client Component |
|---|
| Fetch data, read DB/API | Form, modal, dropdown |
| Handle secret tokens | Event handlers, animation |
| Render static/semi-static layout | useState/useEffect, browser APIs (window, localStorage) |
Keep client components small. Don't "use client" a whole page for one interactive button — extract the button into its own client component and keep the page a Server Component.
5. Vue — Composition API + composables
For full production apps, prefer the Composition API with <script setup> Single File Components. (Options API is fine for simple cases / progressive enhancement.)
components/ → UI
composables/ → reusable pure logic (useX)
- service files → API calls
Use a composable for reusable pure logic; use a component when reusing both logic and layout.
6. State — start local, escalate only when needed
| Need | Use |
|---|
| Component-internal state | useState / ref |
| Reusable state/logic in one feature | custom hook / composable |
| Shared UI state in a subtree | Context (React) / provide-inject (Vue) |
| Cross-app, complex, persisted | Zustand / Pinia / Redux |
| Server state / API cache | TanStack Query (react-query) / similar |
Don't reach for global state (or Redux) on day one of a small app. Server state belongs in a query library, not a global store.
7. API in service files
Never scatter raw fetch/axios across components.
export async function getUsers() {
const res = await http.get('/users')
return res.data
}
export function useUsers() {
return useQuery({ queryKey: ['users'], queryFn: getUsers })
}
Always handle loading, error, and empty states explicitly.
8. Forms validate against a schema
Don't inline long validation inside a component.
- React/Next: react-hook-form + zod (or yup)
- Vue: vee-validate + zod/yup
Keep schemas in *.schema.ts next to the form they validate.
9. Naming
Descriptive, not cryptic. Context from the folder is allowed, but lean explicit.
| Prefer | Avoid |
|---|
UserProfileCard.tsx | Card.tsx |
useCreateBooking.ts | handle.ts |
booking.api.ts | api.ts (bare) |
booking.schema.ts | data.ts |
10. Props
Type props explicitly. When a component takes many related fields, pass the object, not a scatter of primitives.
type ProductCardProps = { product: Product; onSelect?: (p: Product) => void }
<ProductCard id={id} name={name} price={price} image={image} discount={discount} stock={stock} />
11. Anti "god component"
Split a component when it shows these tells:
- longer than ~200 lines
- more than ~3
useEffect/watch
- many
useState/ref
- renders UI and fetches API
- many
if/else business branches
- multiple modals/tables/forms in one file
Decompose along the page seams:
Page
├─ Header
├─ Filter
├─ Table / List
├─ Pagination
└─ Modal / Form
12. Tailwind class hygiene
- If a
className runs past ~5–8 logical groups, extract a component.
- Repeated patterns → a reusable component or variant helper.
- No complex conditional logic inline in
className — use cn().
const cardClassName = cn(
'rounded-xl border p-4',
isActive && 'bg-blue-500 text-white',
isError && 'border-red-500',
)
13. TypeScript
- Enable
strict: true.
- Avoid
any.
- Type props, API responses, and domain models explicitly.
- Prefer union types over enums when a union suffices.
- Validate external data with schemas (zod) — types alone don't guard runtime input.
14. Minimum testing
Don't test everything up front; do cover:
- utilities → unit tests
- important hooks/composables → unit tests
- main forms → validation tests
- critical flows → e2e
Colocate (useBooking.test.ts next to useBooking.ts) or keep tests/unit + tests/e2e — pick one and stay consistent.
15. Accessibility floor
- Use
<button> for actions, not <div onClick>.
- Every input has a label; every image has
alt.
- Modals support keyboard escape + focus trap.
- Forms surface errors clearly.
Remember: one file = one responsibility · UI doesn't own logic · logic → hook/composable · API → service · validation → schema · types separate · Next is server-first · Vue is composable-first. Directory structure comes from the scaffolding skill, not from here.