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.**
Implements pragmatic frontend architecture using React 19, TanStack, and Zustand. Use when building features, setting up state management, or implementing gateway patterns with Context API injection.
/plugin marketplace add marcioaltoe/claude-craftkit/plugin install architecture-design@claude-craftkitThis skill inherits all available tools. When active, it can use any tool Claude has access to.
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:
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.
ALWAYS use MCP servers for up-to-date documentation:
Context7 MCP: Use for comprehensive library documentation, API reference, import statements, and version-specific patterns
Perplexity MCP: Use for architectural research, design patterns, and best practices
Examples of when to use MCP:
For complete frontend tech stack details, see "Tech Stack > Frontend" section in CLAUDE.md
Quick Architecture Reference:
This skill focuses on:
→ 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
Frontend follows a simplified, pragmatic, feature-based architecture.
Core Principles:
Design Philosophy: Simplicity and pragmatism over architectural purity. Remove Clean Architecture complexity (domain/application/infrastructure layers) in favor of direct, maintainable code.
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:
Purpose: Reusable infrastructure used across features.
Example - HTTP Client:
// 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<T>(url: string, config?: AxiosRequestConfig) {
const response = await this.client.get<T>(url, config);
return response.data;
}
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig) {
const response = await this.client.post<T>(url, data, config);
return response.data;
}
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig) {
const response = await this.client.put<T>(url, data, config);
return response.data;
}
async delete<T>(url: string, config?: AxiosRequestConfig) {
const response = await this.client.delete<T>(url, config);
return response.data;
}
}
// Singleton instance
export const httpApi = new HttpApi(import.meta.env.VITE_API_URL);
Purpose: Abstract external resource access. Enable testing with fakes.
Example - Auth Gateway:
// 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<LoginResponse>;
logout(): Promise<void>;
getCurrentUser(): Promise<User>;
}
// Real implementation (HTTP)
export class AuthHttpGateway implements AuthGateway {
async login(request: LoginRequest): Promise<LoginResponse> {
return httpApi.post<LoginResponse>("/auth/login", request);
}
async logout(): Promise<void> {
await httpApi.post("/auth/logout");
}
async getCurrentUser(): Promise<User> {
return httpApi.get<User>("/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<LoginResponse> {
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<void> {
await new Promise((resolve) => setTimeout(resolve, 50));
}
async getCurrentUser(): Promise<User> {
if (this.shouldFail) throw new Error("Unauthorized");
return {
id: "1",
name: "Test User",
email: "test@example.com",
role: "user",
};
}
}
Purpose: Provide gateways to components. Allow override in tests.
Example:
// 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<Gateways | null>(null);
interface GatewayProviderProps {
children: ReactNode;
gateways?: Partial<Gateways>; // Allow override for tests
}
export function GatewayProvider({ children, gateways }: GatewayProviderProps) {
const defaultGateways: Gateways = {
authGateway: new AuthHttpGateway(),
};
const value = { ...defaultGateways, ...gateways };
return (
<GatewayContext.Provider value={value}>{children}</GatewayContext.Provider>
);
}
export function useGateways() {
const context = useContext(GatewayContext);
if (!context) {
throw new Error("useGateways must be used within GatewayProvider");
}
return context;
}
Purpose: Testable business logic, framework-agnostic state management.
Example:
// 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<AuthState>((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:
Purpose: Orchestrate business logic by coordinating gateways, stores, and UI components.
Example:
// 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<string | null>(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 (
<div className="flex min-h-screen items-center justify-center">
<LoginForm
onSubmit={handleLogin}
isLoading={isLoading}
error={formError}
/>
</div>
);
}
Purpose: Pure UI components that receive props and emit events.
Example:
// 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 (
<form onSubmit={handleSubmit} className="w-full max-w-md space-y-4">
<div>
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isLoading}
aria-label="Email"
/>
</div>
<div>
<Input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
aria-label="Password"
/>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? "Loading..." : "Login"}
</Button>
</form>
);
}
Use the RIGHT tool for each state type:
For application-wide client state (auth, theme, preferences):
// 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<ThemeState>((set) => ({
theme: "system",
setTheme: (theme) => set({ theme }),
}));
For data from backend APIs with caching, revalidation, and synchronization:
// 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
});
}
For form management with validation:
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
}
For URL parameters and search params:
// 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 (
<div>
User {userId} - Tab: {tab}
</div>
);
}
For component-specific state:
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
┌─────────────┐
╱ E2E Tests ╲ Few - Critical flows
╱ (Playwright) ╲
╱─────────────────────╲
╱ Integration Tests ╲ Some - Pages with fakes
╱ (Pages + Stores) ╲
╱─────────────────────────╲
╱ Unit Tests ╲ Many - Stores, utils
╱ (Stores, Utils, Comps) ╲
─────────────────────────────
Zustand stores are 100% testable without React:
// 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);
});
});
Test orchestration logic with injected fakes:
// 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(
<GatewayProvider gateways={{ authGateway: fakeAuthGateway }}>
<LoginPage />
</GatewayProvider>
);
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(
<GatewayProvider gateways={{ authGateway: fakeAuthGateway }}>
<LoginPage />
</GatewayProvider>
);
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();
});
});
});
// 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(<LoginForm onSubmit={mock()} />);
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(<LoginForm onSubmit={onSubmit} />);
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/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();
});
});
kebab-case with suffixeskebab-casefeatures/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
Components:
// ✅ Good
export function LoginForm({ onSubmit }: LoginFormProps) {}
// ❌ Avoid
export const loginForm = () => {}; // No arrow function exports
export function Form() {} // Too generic
Pages:
// ✅ Good - "Page" suffix
export function LoginPage() {}
// ❌ Avoid
export function Login() {} // Confuses with component
Stores:
// ✅ Good - "use" + name + "Store"
export const useAuthStore = create<AuthState>(...);
// ❌ Avoid
export const authStore = create(...); // Missing "use" prefix
Gateways:
// ✅ Good - No "I" prefix
export interface AuthGateway {}
export class AuthHttpGateway implements AuthGateway {}
export class AuthFakeGateway implements AuthGateway {}
// ❌ Avoid
export interface IAuthGateway {} // No "I" prefix
// routes/__root.tsx
import { createRootRoute, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: () => (
<div>
<nav>Navigation</nav>
<Outlet />
</div>
),
});
// routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: HomePage,
});
function HomePage() {
return <h1>Home</h1>;
}
import { Link, useNavigate } from "@tanstack/react-router";
export function Navigation() {
const navigate = useNavigate();
return (
<div>
<Link to="/users/$userId" params={{ userId: "123" }}>
View User
</Link>
<button
onClick={() => {
navigate({ to: "/users/$userId", params: { userId: "456" } });
}}
>
Go to User
</button>
</div>
);
}
// ✅ Good - Extract into smaller components
export function UserDashboard() {
return (
<div>
<UserHeader />
<UserStats />
<UserActivity />
</div>
);
}
// ✅ Good
export function UserList() {
const { users, loading, error } = useUsers();
if (loading) return <Loading />;
if (error) return <Error />;
return <div>{/* Render users */}</div>;
}
// ✅ Good - Separate files
// user-card.tsx
export function UserCard() {}
// user-avatar.tsx
export function UserAvatar() {}
NEVER:
any type (use unknown with type guards)ALWAYS:
bun run craft after creating/moving filesWhen helping users, provide:
Frontend architecture focuses on:
Remember: Good architecture makes change easy. Build systems that are simple, testable, and maintainable. Avoid over-engineering. Focus on delivering value.
Reference:
docs/plans/2025-10-24-simplified-frontend-architecture-design.mdproject-standards skillui-designer skillCreating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.