CtrlK
BlogDocsLog inGet started
Tessl Logo

frontend-architecture

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.

70

Quality

87%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

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:

LayerHoldsLives in
UIRendering, markup, presentational statecomponents/
LogicState, effects, data transforms, reusable UI logichooks/ (React) · composables/ (Vue)
DataAPI calls, fetch/axios, cache keyslib/ / service files (*.api.ts)
TypeTypeScript types, domain modelstypes.ts / *.types.ts
ValidationForm/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 typeComfortable range
UI component80–180 lines
Page / screen100–220 lines
Hook / composable40–150 lines
API service50–200 lines
Type / schemaflexible

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.

// ❌ Component owns the data layer
function ProductList() {
  const [products, setProducts] = useState([])
  useEffect(() => { fetch('/api/products').then(r => r.json()).then(setProducts) }, [])
  return <div>{/* render */}</div>
}

// ✅ Component renders; logic lives in a hook
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 ComponentClient Component
Fetch data, read DB/APIForm, modal, dropdown
Handle secret tokensEvent handlers, animation
Render static/semi-static layoutuseState/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

NeedUse
Component-internal stateuseState / ref
Reusable state/logic in one featurecustom hook / composable
Shared UI state in a subtreeContext (React) / provide-inject (Vue)
Cross-app, complex, persistedZustand / Pinia / Redux
Server state / API cacheTanStack 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.

// user.api.ts
export async function getUsers() {
  const res = await http.get('/users')
  return res.data
}

// useUsers.ts
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.

PreferAvoid
UserProfileCard.tsxCard.tsx
useCreateBooking.tshandle.ts
booking.api.tsapi.ts (bare)
booking.schema.tsdata.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 }

// ❌ seven loose props
<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.

Repository
vudovn/ag-kit
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.