--- name: frontend-engineer description: Expert frontend engineering with simplified pragmatic architecture, React 19, TanStack ecosystem, and Zustand state management. **ALWAYS use when implementing ANY frontend features.** Use when setting up project structure, creating pages and state management, designing gateway injection patterns, setting up HTTP communication and routing, organizing feature modules, or optimizing performance. **ALWAYS use when implementing Gateway Pattern (Interface + HTTP + Fake), Context API injection, Zustand stores, TanStack Router, or feature-based architecture.** --- # Frontend Engineer Skill **Purpose**: Expert frontend engineering with simplified pragmatic architecture, React 19, TanStack ecosystem, and Zustand state management. Provides implementation examples for building testable, maintainable, scalable frontend applications. **When to Use**: - Implementing frontend features and components - Setting up project structure - Creating pages and state management - Designing gateway injection patterns - Setting up HTTP communication and routing - Organizing feature modules - Performance optimization and code splitting **NOTE**: For UI component design, Tailwind styling, shadcn/ui setup, responsive layouts, and accessibility implementation, defer to the `ui-designer` skill. This skill focuses on architecture, state management, and business logic orchestration. --- ## Documentation Lookup (MANDATORY) **ALWAYS use MCP servers for up-to-date documentation:** - **Context7 MCP**: Use for comprehensive library documentation, API reference, import statements, and version-specific patterns - When user asks about TanStack Router, Query, Form, Table APIs - For React 19 features and patterns - For Vite configuration and build setup - For Zustand API and patterns - To verify correct import paths, hooks usage, and API patterns - **Perplexity MCP**: Use for architectural research, design patterns, and best practices - When researching pragmatic frontend architectures - For state management strategies and trade-offs - For performance optimization techniques - For folder structure and code organization patterns - For troubleshooting complex architectural issues **Examples of when to use MCP:** - "How to setup TanStack Router file-based routing?" → Use Context7 MCP for TanStack Router docs - "What are React 19 use() hook patterns?" → Use Context7 MCP for React docs - "How to use Zustand with TypeScript?" → Use Context7 MCP for Zustand docs - "Best practices for feature-based architecture in React?" → Use Perplexity MCP for research - "How to configure Vite with React 19?" → Use Context7 MCP for Vite docs --- ## Tech Stack **For complete frontend tech stack details, see "Tech Stack > Frontend" section in CLAUDE.md** **Quick Architecture Reference:** This skill focuses on: - Feature-based architecture (NOT Clean Architecture layers) - Gateway Pattern (Interface + HTTP + Fake) - Context API injection for testability - Zustand for global client state - TanStack Query for server state ### Testing: - **Unit**: Vitest + React Testing Library - **E2E**: Playwright → See `project-standards` skill for complete tech stack → See `ui-designer` skill for UI/UX specific technologies → See `docs/plans/2025-10-24-simplified-frontend-architecture-design.md` for architecture details --- ## Architecture Overview Frontend follows a **simplified, pragmatic, feature-based architecture**. **Core Principles:** 1. **Feature-based organization** - Code grouped by business functionality 2. **Pages as use cases** - Pages orchestrate business logic 3. **Externalized state** - Zustand stores are framework-agnostic, 100% testable 4. **Gateway injection** - Gateways injected via Context API for isolated testing 5. **YAGNI rigorously** - No unnecessary layers or abstractions **Design Philosophy**: Simplicity and pragmatism over architectural purity. Remove Clean Architecture complexity (domain/application/infrastructure layers) in favor of direct, maintainable code. ### Recommended Structure ``` apps/web/src/ ├── app/ # Application setup │ ├── config/ │ │ ├── env.ts # Environment variables │ │ └── index.ts │ ├── providers/ │ │ ├── gateway-provider.tsx # Gateway injection (Context API) │ │ ├── query-provider.tsx # TanStack Query setup │ │ ├── theme-provider.tsx # Theme provider (shadcn/ui) │ │ └── index.ts │ ├── router.tsx # TanStack Router configuration │ └── main.tsx # Application entry point │ ├── features/ # Feature modules (self-contained) │ ├── auth/ │ │ ├── components/ # Pure UI components │ │ │ ├── login-form.tsx │ │ │ └── index.ts │ │ ├── pages/ # Use cases - orchestrate logic │ │ │ ├── login-page.tsx │ │ │ └── index.ts │ │ ├── stores/ # Zustand stores - testable entities │ │ │ ├── auth-store.ts │ │ │ └── index.ts │ │ ├── gateways/ # External resource abstractions │ │ │ ├── auth-gateway.ts # Interface + HTTP implementation │ │ │ ├── auth-gateway.fake.ts # Fake for unit tests │ │ │ └── index.ts │ │ ├── hooks/ # Custom hooks (optional) │ │ │ └── index.ts │ │ ├── types/ # TypeScript types │ │ │ ├── user.ts │ │ │ └── index.ts │ │ └── index.ts # Barrel file - public API │ │ │ ├── dashboard/ │ └── profile/ │ ├── shared/ # Shared code across features │ ├── services/ # Global services │ │ ├── http-api.ts # HTTP client base (Axios wrapper) │ │ ├── storage.ts # LocalStorage/Cookie abstraction │ │ └── index.ts │ ├── components/ │ │ ├── ui/ # shadcn/ui components │ │ └── layout/ # Shared layouts │ ├── hooks/ # Global utility hooks │ ├── lib/ # Utilities and helpers │ │ ├── validators.ts # Zod schemas (common) │ │ ├── formatters.ts │ │ └── index.ts │ └── types/ # Global types │ ├── routes/ # TanStack Router routes │ ├── __root.tsx │ ├── index.tsx │ └── auth/ │ └── login.tsx │ └── index.css ``` **Benefits**: - ✅ Feature isolation - delete folder, remove feature - ✅ No unnecessary layers - direct, maintainable code - ✅ Testable business logic - Zustand stores are pure JS/TS - ✅ Isolated page testing - inject fake gateways - ✅ Clear separation - pages (orchestration), components (UI), stores (state) --- ## Layer Responsibilities (MANDATORY) ### 1. Shared Services Layer **Purpose**: Reusable infrastructure used across features. **Example - HTTP Client**: ```typescript // shared/services/http-api.ts import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios"; export class HttpApi { private client: AxiosInstance; constructor(baseURL: string) { this.client = axios.create({ baseURL, timeout: 10000, headers: { "Content-Type": "application/json" }, }); // Auto-inject auth token this.client.interceptors.request.use((config) => { const token = localStorage.getItem("auth_token"); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); // Handle 401 globally this.client.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { localStorage.removeItem("auth_token"); window.location.href = "/auth/login"; } return Promise.reject(error); } ); } async get(url: string, config?: AxiosRequestConfig) { const response = await this.client.get(url, config); return response.data; } async post(url: string, data?: unknown, config?: AxiosRequestConfig) { const response = await this.client.post(url, data, config); return response.data; } async put(url: string, data?: unknown, config?: AxiosRequestConfig) { const response = await this.client.put(url, data, config); return response.data; } async delete(url: string, config?: AxiosRequestConfig) { const response = await this.client.delete(url, config); return response.data; } } // Singleton instance export const httpApi = new HttpApi(import.meta.env.VITE_API_URL); ``` --- ### 2. Gateway Layer **Purpose**: Abstract external resource access. Enable testing with fakes. **Example - Auth Gateway**: ```typescript // features/auth/gateways/auth-gateway.ts import { httpApi } from "@/shared/services/http-api"; import type { User } from "../types/user"; export interface LoginRequest { email: string; password: string; } export interface LoginResponse { user: User; token: string; } // Gateway interface (contract) export interface AuthGateway { login(request: LoginRequest): Promise; logout(): Promise; getCurrentUser(): Promise; } // Real implementation (HTTP) export class AuthHttpGateway implements AuthGateway { async login(request: LoginRequest): Promise { return httpApi.post("/auth/login", request); } async logout(): Promise { await httpApi.post("/auth/logout"); } async getCurrentUser(): Promise { return httpApi.get("/auth/me"); } } // Fake implementation for unit tests export class AuthFakeGateway implements AuthGateway { private shouldFail = false; setShouldFail(value: boolean) { this.shouldFail = value; } async login(request: LoginRequest): Promise { await new Promise((resolve) => setTimeout(resolve, 100)); if (this.shouldFail) { throw new Error("Invalid credentials"); } if ( request.email === "test@example.com" && request.password === "password123" ) { return { user: { id: "1", name: "Test User", email: "test@example.com", role: "user", }, token: "fake-token-123", }; } throw new Error("Invalid credentials"); } async logout(): Promise { await new Promise((resolve) => setTimeout(resolve, 50)); } async getCurrentUser(): Promise { if (this.shouldFail) throw new Error("Unauthorized"); return { id: "1", name: "Test User", email: "test@example.com", role: "user", }; } } ``` --- ### 3. Gateway Injection (Context API) **Purpose**: Provide gateways to components. Allow override in tests. **Example**: ```typescript // app/providers/gateway-provider.tsx import { createContext, useContext, type ReactNode } from "react"; import { AuthGateway, AuthHttpGateway, } from "@/features/auth/gateways/auth-gateway"; interface Gateways { authGateway: AuthGateway; // Add more gateways as needed } const GatewayContext = createContext(null); interface GatewayProviderProps { children: ReactNode; gateways?: Partial; // Allow override for tests } export function GatewayProvider({ children, gateways }: GatewayProviderProps) { const defaultGateways: Gateways = { authGateway: new AuthHttpGateway(), }; const value = { ...defaultGateways, ...gateways }; return ( {children} ); } export function useGateways() { const context = useContext(GatewayContext); if (!context) { throw new Error("useGateways must be used within GatewayProvider"); } return context; } ``` --- ### 4. Store Layer (Zustand) **Purpose**: Testable business logic, framework-agnostic state management. **Example**: ```typescript // features/auth/stores/auth-store.ts import { create } from "zustand"; import type { User } from "../types/user"; interface AuthState { user: User | null; isAuthenticated: boolean; isLoading: boolean; error: string | null; setUser: (user: User | null) => void; setLoading: (loading: boolean) => void; setError: (error: string | null) => void; reset: () => void; } export const useAuthStore = create((set) => ({ user: null, isAuthenticated: false, isLoading: false, error: null, setUser: (user) => set({ user, isAuthenticated: user !== null }), setLoading: (isLoading) => set({ isLoading }), setError: (error) => set({ error }), reset: () => set({ user: null, isAuthenticated: false, isLoading: false, error: null }), })); ``` **Why Zustand:** - ✅ Minimal boilerplate - ✅ Excellent TypeScript support - ✅ Framework-agnostic (100% testable without React) - ✅ Large community and ecosystem - ✅ Superior DX compared to alternatives --- ### 5. Page Layer (Use Cases) **Purpose**: Orchestrate business logic by coordinating gateways, stores, and UI components. **Example**: ```typescript // features/auth/pages/login-page.tsx import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { useGateways } from "@/app/providers/gateway-provider"; import { useAuthStore } from "../stores/auth-store"; import { LoginForm } from "../components/login-form"; export function LoginPage() { const navigate = useNavigate(); const { authGateway } = useGateways(); // Injected gateway const { setUser, setLoading, setError, isLoading, error } = useAuthStore(); const [formError, setFormError] = useState(null); const handleLogin = async (email: string, password: string) => { setLoading(true); setError(null); setFormError(null); try { const { user, token } = await authGateway.login({ email, password }); localStorage.setItem("auth_token", token); setUser(user); navigate({ to: "/dashboard" }); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Login failed"; setError(errorMessage); setFormError(errorMessage); } finally { setLoading(false); } }; return (
); } ``` --- ### 6. Component Layer **Purpose**: Pure UI components that receive props and emit events. **Example**: ```typescript // features/auth/components/login-form.tsx import { useState } from "react"; import { Button } from "@/shared/components/ui/button"; import { Input } from "@/shared/components/ui/input"; interface LoginFormProps { onSubmit: (email: string, password: string) => void; isLoading?: boolean; error?: string | null; } export function LoginForm({ onSubmit, isLoading = false, error, }: LoginFormProps) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(email, password); }; return (
setEmail(e.target.value)} disabled={isLoading} aria-label="Email" />
setPassword(e.target.value)} disabled={isLoading} aria-label="Password" />
{error &&

{error}

}
); } ``` --- ## State Management Strategy (MANDATORY) **Use the RIGHT tool for each state type:** ### 1. Global Client State → Zustand For application-wide client state (auth, theme, preferences): ```typescript // shared/stores/theme-store.ts import { create } from "zustand"; export type Theme = "light" | "dark" | "system"; interface ThemeState { theme: Theme; setTheme: (theme: Theme) => void; } export const useThemeStore = create((set) => ({ theme: "system", setTheme: (theme) => set({ theme }), })); ``` ### 2. Server State → TanStack Query For data from backend APIs with caching, revalidation, and synchronization: ```typescript // features/users/hooks/use-users-query.ts import { useQuery } from "@tanstack/react-query"; import { useGateways } from "@/app/providers/gateway-provider"; export function useUsersQuery() { const { userGateway } = useGateways(); return useQuery({ queryKey: ["users"], queryFn: () => userGateway.getUsers(), staleTime: 5 * 60 * 1000, // 5 minutes }); } ``` ### 3. Form State → TanStack Form For form management with validation: ```typescript import { useForm } from "@tanstack/react-form"; import { zodValidator } from "@tanstack/zod-form-adapter"; import { z } from "zod"; const userSchema = z.object({ name: z.string().min(2), email: z.string().email(), }); export function UserForm() { const form = useForm({ defaultValues: { name: "", email: "" }, validatorAdapter: zodValidator(), validators: { onChange: userSchema }, onSubmit: async ({ value }) => console.log(value), }); // ... render form } ``` ### 4. URL State → TanStack Router For URL parameters and search params: ```typescript // routes/users/$userId.tsx import { createFileRoute } from "@tanstack/react-router"; import { z } from "zod"; const userSearchSchema = z.object({ tab: z.enum(["profile", "settings"]).optional(), }); export const Route = createFileRoute("/users/$userId")({ validateSearch: userSearchSchema, component: UserDetail, }); function UserDetail() { const { userId } = Route.useParams(); const { tab = "profile" } = Route.useSearch(); return (
User {userId} - Tab: {tab}
); } ``` ### 5. Local Component State → useState/useReducer For component-specific state: ```typescript export function Counter() { const [count, setCount] = useState(0); return (

Count: {count}

); } ``` --- ## Testing Strategy (MANDATORY) ### Test Pyramid ``` ┌─────────────┐ ╱ E2E Tests ╲ Few - Critical flows ╱ (Playwright) ╲ ╱─────────────────────╲ ╱ Integration Tests ╲ Some - Pages with fakes ╱ (Pages + Stores) ╲ ╱─────────────────────────╲ ╱ Unit Tests ╲ Many - Stores, utils ╱ (Stores, Utils, Comps) ╲ ───────────────────────────── ``` ### Store Tests (Unit - High Priority) Zustand stores are 100% testable without React: ```typescript // features/auth/stores/auth-store.test.ts import { describe, it, expect, beforeEach } from "bun:test"; import { useAuthStore } from "./auth-store"; describe("AuthStore", () => { beforeEach(() => { useAuthStore.getState().reset(); }); it("should start with empty state", () => { const state = useAuthStore.getState(); expect(state.user).toBeNull(); expect(state.isAuthenticated).toBe(false); }); it("should update user and isAuthenticated when setting user", () => { const user = { id: "1", name: "Test", email: "test@example.com", role: "user", }; useAuthStore.getState().setUser(user); expect(useAuthStore.getState().user).toEqual(user); expect(useAuthStore.getState().isAuthenticated).toBe(true); }); }); ``` ### Page Tests (Integration with Fake Gateways) Test orchestration logic with injected fakes: ```typescript // features/auth/pages/login-page.test.tsx import { describe, it, expect, beforeEach } from "bun:test"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { GatewayProvider } from "@/app/providers/gateway-provider"; import { AuthFakeGateway } from "../gateways/auth-gateway"; import { LoginPage } from "./login-page"; import { useAuthStore } from "../stores/auth-store"; describe("LoginPage", () => { let fakeAuthGateway: AuthFakeGateway; beforeEach(() => { fakeAuthGateway = new AuthFakeGateway(); useAuthStore.getState().reset(); }); it("should login successfully and update store", async () => { const user = userEvent.setup(); render( ); await user.type(screen.getByLabelText(/email/i), "test@example.com"); await user.type(screen.getByLabelText(/password/i), "password123"); await user.click(screen.getByRole("button", { name: /login/i })); await waitFor(() => { expect(useAuthStore.getState().isAuthenticated).toBe(true); }); }); it("should show error when login fails", async () => { const user = userEvent.setup(); fakeAuthGateway.setShouldFail(true); render( ); await user.type(screen.getByLabelText(/email/i), "wrong@example.com"); await user.type(screen.getByLabelText(/password/i), "wrong"); await user.click(screen.getByRole("button", { name: /login/i })); await waitFor(() => { expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument(); }); }); }); ``` ### Component Tests (Unit - Pure UI) ```typescript // features/auth/components/login-form.test.tsx import { describe, it, expect, mock } from "bun:test"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { LoginForm } from "./login-form"; describe("LoginForm", () => { it("should render email and password fields", () => { render(); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); }); it("should call onSubmit with correct values", async () => { const user = userEvent.setup(); const onSubmit = mock(); render(); await user.type(screen.getByLabelText(/email/i), "test@example.com"); await user.type(screen.getByLabelText(/password/i), "password123"); await user.click(screen.getByRole("button", { name: /login/i })); expect(onSubmit).toHaveBeenCalledWith("test@example.com", "password123"); }); }); ``` ### E2E Tests (Playwright - Critical Flows) ```typescript // e2e/auth/login.spec.ts import { test, expect } from "@playwright/test"; test.describe("Login Flow", () => { test("should login and redirect to dashboard", async ({ page }) => { await page.goto("/auth/login"); await page.fill('input[name="email"]', "test@example.com"); await page.fill('input[name="password"]', "password123"); await page.click('button:has-text("Login")'); await expect(page).toHaveURL("/dashboard"); await expect(page.locator("text=Welcome, Test User")).toBeVisible(); }); }); ``` --- ## Naming Conventions (MANDATORY) ### Files and Folders - **Files**: `kebab-case` with suffixes - **Folders**: `kebab-case` ``` features/auth/ ├── components/ │ ├── login-form.tsx # Component: kebab-case │ └── index.ts ├── pages/ │ ├── login-page.tsx # Suffix: -page.tsx │ └── index.ts ├── stores/ │ ├── auth-store.ts # Suffix: -store.ts │ └── index.ts ├── gateways/ │ ├── auth-gateway.ts # Suffix: -gateway.ts │ ├── auth-gateway.fake.ts # Suffix: -gateway.fake.ts │ └── index.ts ``` ### Code Elements **Components**: ```typescript // ✅ Good export function LoginForm({ onSubmit }: LoginFormProps) {} // ❌ Avoid export const loginForm = () => {}; // No arrow function exports export function Form() {} // Too generic ``` **Pages**: ```typescript // ✅ Good - "Page" suffix export function LoginPage() {} // ❌ Avoid export function Login() {} // Confuses with component ``` **Stores**: ```typescript // ✅ Good - "use" + name + "Store" export const useAuthStore = create(...); // ❌ Avoid export const authStore = create(...); // Missing "use" prefix ``` **Gateways**: ```typescript // ✅ Good - No "I" prefix export interface AuthGateway {} export class AuthHttpGateway implements AuthGateway {} export class AuthFakeGateway implements AuthGateway {} // ❌ Avoid export interface IAuthGateway {} // No "I" prefix ``` --- ## TanStack Router Patterns (MANDATORY) ### File-Based Routing ```typescript // routes/__root.tsx import { createRootRoute, Outlet } from "@tanstack/react-router"; export const Route = createRootRoute({ component: () => (
), }); // routes/index.tsx import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ component: HomePage, }); function HomePage() { return

Home

; } ``` ### Type-Safe Navigation ```typescript import { Link, useNavigate } from "@tanstack/react-router"; export function Navigation() { const navigate = useNavigate(); return (
View User
); } ``` --- ## Component Organization Principles (MANDATORY) ### 1. Keep Components Small (< 150 lines) ```typescript // ✅ Good - Extract into smaller components export function UserDashboard() { return (
); } ``` ### 2. Extract Logic into Hooks (< 20 lines in component) ```typescript // ✅ Good export function UserList() { const { users, loading, error } = useUsers(); if (loading) return ; if (error) return ; return
{/* Render users */}
; } ``` ### 3. One Component Per File ```typescript // ✅ Good - Separate files // user-card.tsx export function UserCard() {} // user-avatar.tsx export function UserAvatar() {} ``` --- ## Critical Rules **NEVER:** - Use Clean Architecture layers (domain/application/infrastructure) - Import stores in components (pass data via props) - Call HTTP directly from components (use gateways) - Use `any` type (use `unknown` with type guards) - Skip TypeScript types - Use npm, pnpm, yarn (use Bun) - Create components > 150 lines - Keep logic > 20 lines in components **ALWAYS:** - Organize by features (feature-based structure) - Pages orchestrate logic (use gateways + stores) - Externalize state to Zustand stores - Inject gateways via Context API - Write tests for stores (unit) and pages (integration with fakes) - Use functional components with TypeScript - One component per file - Extract logic into custom hooks - Use Bun for all package management - Run `bun run craft` after creating/moving files --- ## Deliverables When helping users, provide: 1. **Feature Folder Structure**: Organized with pages/, components/, stores/, gateways/ 2. **Gateway Definitions**: Interface + HTTP implementation + Fake implementation 3. **Gateway Provider**: Context API setup for dependency injection 4. **Zustand Stores**: Framework-agnostic state management with actions 5. **Pages**: Orchestration logic using gateways and stores 6. **Components**: Pure UI components with props 7. **Router Configuration**: TanStack Router setup with type safety 8. **Test Examples**: Store tests, page tests with fakes, component tests, E2E 9. **Configuration Files**: Vite, TypeScript, TanStack configurations --- ## Summary Frontend architecture focuses on: 1. **Simplicity**: No unnecessary layers, direct code, pragmatic patterns 2. **Testability**: Zustand stores (pure), pages with fake gateways, isolated components 3. **Maintainability**: Feature-based structure, clear responsibilities, consistent patterns 4. **Type Safety**: Strong typing, Zod validation, TanStack Router 5. **Performance**: Code splitting, lazy loading, optimistic updates 6. **User Experience**: Error handling, loading states, toast notifications **Remember**: Good architecture makes change easy. Build systems that are simple, testable, and maintainable. Avoid over-engineering. Focus on delivering value. --- **Reference**: - Architecture design: `docs/plans/2025-10-24-simplified-frontend-architecture-design.md` - Project standards: `project-standards` skill - UI/UX: `ui-designer` skill