kao-vite-structure

Installation
SKILL.md

Vite + React + TypeScript + shadcn/ui Project Structure

Stack: Vite · React · TypeScript · shadcn/ui (Radix) · TanStack Query · Zustand · React Hook Form + Zod · React Router v7 · Vitest · Playwright · Biome

This skill defines the canonical folder layout, naming conventions, and architectural rules for the project. Every file you create, move, or modify should follow these patterns.


Project Tree

my-app/
├── public/
├── src/
│   ├── app/                    # Runtime wiring only (App, router, providers)
│   ├── assets/
│   ├── components/
│   │   ├── ui/                 # shadcn — CLI-managed, never edit manually
│   │   ├── layout/             # App shell + layout route components
│   │   └── common/             # Reusable product-level components (2+ features)
│   ├── config/                 # Static config: path.ts, menu.ts, url.ts
│   ├── features/
│   │   └── [feature-name]/     # Self-contained vertical slices
│   ├── hooks/                  # Global hooks (used in 2+ features)
│   ├── lib/                    # Infrastructure: utils.ts, api-client.ts
│   ├── pages/
│   │   └── [feature-name]/     # Page components + route definitions
│   ├── styles/globals.css
│   ├── types/                  # Global types (used in 2+ features)
│   ├── test-setup.ts
│   └── main.tsx
├── tests/
│   ├── e2e/                    # Playwright specs, mirrors features/
│   └── fixtures/
├── biome.json
├── components.json
├── playwright.config.ts
├── vite.config.ts
├── tsconfig.json
└── package.json

Naming Conventions

Files — always kebab-case

Type Pattern Example
Component kebab-case.tsx feature-card.tsx
Page kebab-case-page.tsx feature-detail-page.tsx
Hook use-kebab-case.ts use-feature-list.ts
Utility kebab-case.ts format-status.ts
Type kebab-case.type.ts feature.type.ts
Schema kebab-case.schema.ts feature-create.schema.ts
Store kebab-case.store.ts feature.store.ts
API kebab-case.api.ts feature.api.ts
Query kebab-case.query.ts feature.query.ts
Routes kebab-case.routes.tsx feature.routes.tsx
Unit test source-name.test.tsx feature-card.test.tsx
E2e test kebab-case.spec.ts feature-create.spec.ts

Exports

  • PascalCase for components: export function FeatureCard() {}
  • camelCase for everything else: export function useFeatureList() {}, export function formatStatus() {}

Folders

  • Plural for countable collections: components/, hooks/, pages/, types/, utils/, schemas/, stores/, features/, assets/, styles/
  • Singular for uncountable/collective concepts: api/, lib/, config/, app/

Folder Responsibilities

src/app/ — Runtime Wiring

Only three files: App.tsx, router.tsx, providers.tsx. No business logic, no UI components. Layout components belong in components/layout/, not here.

  • router.tsx imports layouts from components/layout/ and route arrays from pages/ — never imports page components directly
  • providers.tsx wraps QueryClient, Toaster, ThemeProvider, etc.

src/config/ — Static Configuration

  • path.ts — single source of truth for all frontend route strings
  • url.ts — reads from import.meta.env, never hardcodes URLs
  • menu.ts — imports from path.ts, never hardcodes route strings
  • No imports from features/ or components/

src/components/

  • ui/ — CLI-owned shadcn components. Add via npx shadcn@latest add <name>. Never edit manually.
  • layout/ — app shell and layout route components (anything wrapping <Outlet />): app-shell.tsx, auth-layout.tsx, public-layout.tsx, sidebar.tsx, header.tsx, footer.tsx
  • common/ — reusable product-level components used in 2+ places. If only one feature uses it, keep it in that feature.

src/features/[feature-name]/ — Vertical Slices

Each feature is self-contained:

features/feature-a/
├── components/          # Feature-local UI
├── hooks/               # Feature-local hooks
├── api/                 # API calls + TanStack Query definitions
│   ├── feature-a.api.ts
│   └── feature-a.query.ts
├── schemas/             # Zod validation schemas
├── stores/              # Zustand slices
├── types/               # TypeScript types
├── utils/               # Pure helpers
└── index.ts             # Public API — only export what pages need

Scale to complexity:

  • Small (1–2 components): components/, index.ts
  • Medium: + hooks/, api/, types/
  • Large: full structure with schemas/, stores/, utils/

Complex Components

When a component needs sub-components, local hooks, or local state, give it a subfolder:

components/
└── feature-a-form/
    ├── index.tsx              # Re-exports single public component
    ├── feature-a-form.tsx     # Root orchestrator
    ├── steps/                 # Sub-components
    ├── components/            # Form-local shared UI
    └── hooks/                 # Form-local hooks

Callers import transparently — import { FeatureAForm } from "~/features/feature-a" resolves through the subfolder's index.tsx.

API Layer Pattern

// api/feature-a.api.ts — fetch functions
export const featureAApi = {
  getAll: () => apiClient.get<FeatureA[]>("/feature-a"),
  getById: (id: string) => apiClient.get<FeatureA>(`/feature-a/${id}`),
  create: (data: CreateFeatureADto) => apiClient.post("/feature-a", data),
}

// api/feature-a.query.ts — TanStack Query keys + options
export const featureAKeys = {
  all: ["feature-a"] as const,
  detail: (id: string) => ["feature-a", id] as const,
}
export const featureAQueries = {
  list: () => queryOptions({ queryKey: featureAKeys.all, queryFn: () => featureAApi.getAll() }),
  detail: (id: string) => queryOptions({ queryKey: featureAKeys.detail(id), queryFn: () => featureAApi.getById(id) }),
}

src/pages/[feature-name]/ — Route-Level Components

Each page folder owns its route definitions:

pages/feature-a/
├── feature-a-page.tsx
├── feature-a-detail-page.tsx
└── feature-a.routes.tsx       # RouteObject[] for this feature
  • One page file per route, suffixed with -page
  • Pages are thin orchestration — no business logic, no direct API calls
  • Import from feature index.ts only
  • Route definitions live in [feature].routes.tsx, never directly in app/router.tsx
// pages/feature-a/feature-a.routes.tsx
export const featureARoutes: RouteObject[] = [
  { path: "feature-a", element: <FeatureAPage /> },
  { path: "feature-a/:id", element: <FeatureADetailPage /> },
]

src/hooks/, src/types/ — Global Shared

Only promote hooks/types here when used across 2+ features. Feature-specific items stay in the feature.

src/lib/ — Infrastructure

  • utils.ts — must export cn() (required by shadcn)
  • api-client.ts — Axios instance reading base URL from ~/config/url
  • No business logic

tests/ — E2e Only

Playwright specs in tests/e2e/, mirroring features/ structure. Vitest unit tests are colocated next to source files in src/.


Import Rules

These rules prevent circular dependencies and keep the architecture clean:

// Pages import from feature index — never deep-import
import { FeatureAList } from "~/features/feature-a"          // correct
import { FeatureAList } from "~/features/feature-a/components/feature-a-list"  // wrong

// Cross-feature imports always via index
import { useAuth } from "~/features/auth"                     // correct
import { useAuth } from "~/features/auth/hooks/use-auth"      // wrong

// Features import from shared lib, ui, and config
import { cn } from "~/lib/utils"
import { Button } from "~/components/ui/button"
import { API_URLS } from "~/config/url"

Router Composition

app/router.tsx is composition only — it imports layouts from components/layout/ and route arrays from pages/:

import { PublicLayout } from "~/components/layout/public-layout"
import { AuthLayout } from "~/components/layout/auth-layout"
import { authRoutes } from "~/pages/auth/auth.routes"
import { featureARoutes } from "~/pages/feature-a/feature-a.routes"

export const router = createBrowserRouter([
  { element: <PublicLayout />, children: authRoutes },
  { element: <AuthLayout />, children: [...homeRoutes, ...featureARoutes] },
  ...errorRoutes,
])

Adding a new feature = one import + one spread in the right layout group.


shadcn/ui Rules

  • Never manually edit components/ui/ — use npx shadcn@latest add <component>
  • Use semantic color tokens only (bg-background, text-muted-foreground) — never raw Tailwind colors (bg-white, text-gray-500)
  • Spacing with gap-*, never space-y-*
  • Equal dimensions with size-*, never w-* h-* together
  • Icons in buttons use data-icon attribute, no sizing classes on icons:
<Button>
  <PlusIcon data-icon="inline-start" />
  Create
</Button>

The 7 Rules That Matter Most

  1. Features are siblings, never parents. Cross-feature imports go through index.ts only.
  2. Pages are thin. No logic, no API calls — only composition.
  3. Each pages folder owns its routes. Route definitions live in [feature].routes.tsx next to the pages.
  4. router.tsx is composition only. Imports layouts and route arrays — never page components.
  5. Layout components belong in components/layout/. They are UI, not runtime wiring.
  6. components/ui/ is CLI-owned. Never edit it manually.
  7. One source of truth per concern. Routes in config/path.ts, API URLs in config/url.ts, menu in config/menu.ts.
Related skills
Installs
1
Repository
kaotypr/skills
First Seen
Apr 4, 2026