| name | frontend-engineer |
| description | Expert in Template Project frontend architecture, React patterns, TypeScript, TanStack Query, Zustand, and component development with strong type safety. Use when building UI components, implementing data fetching, managing state, writing forms, styling with Tailwind, testing React components, or integrating with backend APIs. |
Template Frontend Engineer
Expert knowledge of frontend patterns and architecture for the Template Project.
Core Architecture
Technology Stack
- Framework: React 18 with TypeScript
- State Management:
- Server State: TanStack Query (React Query)
- Client State: Zustand with Immer middleware
- Styling: Tailwind CSS + lucide-react icons
- Forms: react-hook-form + Zod validation
- Testing: Vitest + React Testing Library + MSW v2
- HTTP Client: Custom fetch wrapper with credentials
Data Flow Architecture
Component (React)
|
State Decision:
├── Server Data -> TanStack Query Hook -> API Service -> fetch
| |
| Type-Safe Response (@todos/schema/api)
|
└── UI State -> Zustand Store (with Immer)
|
Type-Safe Types (from @todos/schema)
Key Flow:
- Component calls TanStack Query hook or Zustand store
- Hook uses API service layer
- Request/Response types from
@todos/schema/src/api/
- UI state (selections, UI flags) in Zustand
- Server state (data, caching) in TanStack Query
CRITICAL: Where to Look Before Making Changes
Pattern References (ALWAYS CHECK THESE FIRST)
| Pattern | Primary Reference | Notes |
|---|
| Reusable Components | packages/components/src/ | ALWAYS CHECK FIRST |
| API Types (Request/Response) | packages/schema/src/api/ | CHECK HERE FIRST |
| API Service Layer | packages/ui/src/services/api.ts | Type-safe API calls |
| Zustand Store | packages/ui/src/stores/todoStore.ts | State management with Immer |
| Component Test | packages/ui/src/components/*.test.tsx | Testing patterns |
| Domain Types | packages/schema/src/types.ts | Shared type definitions |
Type Safety & Validation
Use Enums and Constants, Not Magic Strings
Never use magic strings. Always use enums, constants, or Zod enums for:
- API endpoints: Define in constants file, not inline
- Status values: Use Zod enums for validation + type inference
- Action types: Use string literal unions or enums
- Route paths: Define as constants
- Storage keys: Define as const object, not inline strings
Example - Route Constants:
export const ROUTES = {
HOME: '/',
TODOS: '/todos',
TODO_DETAIL: (id: string) => `/todos/${id}`,
} as const;
navigate(ROUTES.TODO_DETAIL(todoId));
navigate(`/todos/${todoId}`);
Example - Zod Enums:
const StatusSchema = z.enum(['pending', 'completed']);
type Status = z.infer<typeof StatusSchema>;
type Status = string;
Type Generation Rules
- Check Schema First:
packages/schema/src/api/
- Create in @todos/schema: All request/response types
- Add Zod Schemas: For runtime validation
- Export from Index: Make available to both server and client
Component Development
Component Decision Flow
Need Component?
|
1. Check packages/components/ first <- **ALWAYS START HERE**
├── Exists & works? -> Use it
└── Doesn't exist or needs changes
|
2. Is it reusable across features?
├── Yes -> Create/extend in packages/components/
└── No -> Create in packages/ui/src/components/
Component Structure
packages/components/ # Reusable component library
├── src/
│ ├── ui/ # shadcn/ui base components
│ └── index.ts # Public exports
packages/ui/ # Application-specific
├── src/
│ ├── components/ # App-specific components
│ ├── pages/ # Route pages
│ ├── stores/ # Zustand stores
│ └── services/ # API services
Component Pattern
import { useState } from 'react';
import { useTodoStore } from '../stores/todoStore';
import { Todo } from '@todos/schema';
interface TodoListProps {
onToggle: (id: string) => void;
onDelete: (id: string) => void;
}
export function TodoList({ onToggle, onDelete }: TodoListProps) {
const { todos, isLoading, error } = useTodoStore();
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return (
<ul className="space-y-2">
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
=
=
/>
))}
);
}
Key Patterns:
- ALWAYS check
packages/components/ first before creating
- Use lucide-react for icons
- Tailwind for styling
- Loading and error states built-in
- Prop-based callbacks
State Management Decision Tree
When to Use What?
State Needed -> From Server?
├── Yes -> Needs Caching?
│ ├── Yes -> TanStack Query
│ └── No -> Direct API Call in useEffect
└── No -> Shared Across Components?
├── Yes -> Complex Logic?
│ ├── Yes -> Zustand Store
│ └── No -> React Context
└── No -> Local useState
TanStack Query Patterns
export function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: () => api.getTodos(),
staleTime: 5 * 60 * 1000,
});
}
function TodoPage() {
const { data: todos, isLoading, error } = useTodos();
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage />;
return <TodoList todos={todos} />;
}
Zustand Store Patterns
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { api } from '../services/api';
import { Todo } from '@todos/schema';
interface TodoState {
todos: Todo[];
isLoading: boolean;
error: string | null;
fetchTodos: () => Promise<void>;
addTodo: (title: string, description?: string) => Promise<void>;
toggleTodo: (id: string) => Promise<void>;
deleteTodo: (id: string) => Promise<void>;
clearError: () => void;
}
export const useTodoStore = create<>()(
( ({
: [],
: ,
: ,
: () => {
({ : , : });
{
todos = api.();
({ todos, : });
} (error) {
({ : , : });
}
},
: (title, description) => {
{
newTodo = api.({ title, description });
( {
state..(newTodo);
});
} (error) {
({ : });
}
},
: (id) => {
{
updatedTodo = api.(id);
( {
index = state..( t. === id);
(index !== -) {
state.[index] = updatedTodo;
}
});
} (error) {
({ : });
}
},
: (id) => {
{
api.(id);
( {
state. = state..( t. !== id);
});
} (error) {
({ : });
}
},
: ({ : }),
}))
);
Key Patterns:
- Use Immer middleware for immutable updates
- Keep UI state separate from server state
- Action names for debugging
API Integration Patterns
Service Layer Structure
import {
Todo,
CreateTodoInput,
UpdateTodoInput,
} from '@todos/schema';
const API_BASE = import.meta.env.VITE_API_URL || '';
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${API_BASE}${url}`, {
headers: {
'Content-Type': 'application/json',
},
...options,
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.data;
}
export const api = {
getTodos: () => request<Todo[]>('/api/todos'),
getTodo: (id: string) => request<>(),
:
request<>(, {
: ,
: .(data),
}),
:
request<>(, {
: ,
: .(data),
}),
:
request<>(, {
: ,
}),
:
request<>(, {
: ,
}),
};
Testing Patterns
Testing Priority Order (CRITICAL)
- Unit Tests First - Test logic in isolation (functions, hooks, components)
- MSW Integration Tests - Test API interactions
- E2E Tests Last - Only for critical user flows
ALWAYS write or update unit tests BEFORE MSW or integration tests.
Component Testing Setup
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
import { TodoList } from './TodoList';
describe('TodoList', () => {
const mockTodos = [
{ id: '1', title: 'Test Todo', completed: false },
{ id: '2', title: 'Completed Todo', completed: true },
];
it('should render todos', () => {
render(<TodoList todos={mockTodos} onToggle={vi.fn()} onDelete={vi.fn()} />);
expect(screen.getByText('Test Todo')).toBeInTheDocument();
expect(screen.getByText('Completed Todo')).toBeInTheDocument();
});
it(, () => {
user = userEvent.();
onToggle = vi.();
();
user.(screen.()[]);
(onToggle).();
});
});
Zustand Store Testing
import { renderHook, act } from '@testing-library/react';
import { useTodoStore } from '../todoStore';
describe('TodoStore', () => {
beforeEach(() => {
useTodoStore.setState({
todos: [],
isLoading: false,
error: null,
});
});
it('should add todo', async () => {
const { result } = renderHook(() => useTodoStore());
await act(async () => {
await result.current.addTodo('New Todo');
});
expect(result.current.todos).toHaveLength(1);
});
});
Form Handling Patterns
react-hook-form + Zod Integration
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createTodoSchema, CreateTodoInput } from '@todos/schema';
export function TodoForm({ onSubmit }: { onSubmit: (data: CreateTodoInput) => void }) {
const form = useForm<CreateTodoInput>({
resolver: zodResolver(createTodoSchema),
defaultValues: {
title: '',
description: '',
},
});
const handleSubmit = form.handleSubmit(async (data) => {
try {
await onSubmit(data);
form.reset();
} catch (error) {
form.setError('root', {
message: 'Failed to create todo',
});
}
});
return (
<form onSubmit={handleSubmit}>
<input
{...form.register('title')}
placeholder="What needs to be done?"
/>
{form.formState.errors.title && (
{form.formState.errors.title.message}
)}
{form.formState.isSubmitting ? 'Adding...' : 'Add Todo'}
);
}
Common Commands
cd packages/ui
pnpm dev
pnpm build
pnpm type-check
pnpm test:changed
pnpm test:unit
pnpm test
pnpm vitest run --no-coverage src/components/TodoList.test.tsx
pnpm vitest --no-coverage src/components/TodoList.test.tsx
pnpm test:changed
Testing Strategy:
- Development:
pnpm test:changed (only tests what you changed)
- Focused work:
pnpm vitest run --no-coverage [file]
- Watch mode:
pnpm vitest --no-coverage [file] (auto-rerun)
- Pre-commit:
pnpm test:unit (fast, no e2e)
- Pre-push:
pnpm test (full suite)
Quick Reference
Essential Hooks
useTodoStore();
useQuery();
useMutation();
useForm();
Component Checklist
Performance Checklist
Critical Reminders
Before Any Development:
- Check
packages/components/ for reusable components
- Check
packages/schema/src/api/ for request/response types
- Write unit tests BEFORE integration/MSW tests
- Use
pnpm test:changed during development
Architecture Flow:
Component -> Zustand Store -> API Service -> @schema/api types
or
Component -> TanStack Query -> API Service -> @schema/api types
Remember: Maintain type safety across all layers. Always validate with Zod at boundaries. Check component library before creating new components.