Frontend Engineering
Enforces production-grade React/Next.js frontend architecture — clean component structure, scalable CSS, correct state management, and performance patterns that eliminate AI-slop and amateur mistakes. Primary constraint: every component must be reusable, every style must be variable-mapped, and every state update must be traceable and loop-free.
Core Rules
Rule: Isolate frontend and backend completely — separate root folders, separate servers. Why: mixing them creates ambiguous security boundaries and makes credential leaks far more likely.
Rule: Never make database calls from React/Next.js files. Why: this exposes
credentials in the browser and bypasses all backend security validation.
All data fetching goes through the services/ API layer only.
Rule: Never use any type in TypeScript. Why: any disables the type
system entirely — bugs that TypeScript would catch at compile time become
runtime crashes in production.
Rule: Never store JWT access tokens in localStorage or sessionStorage. Why: XSS scripts can read these stores; stolen tokens grant full account access. Access tokens stay in React state/context only.
Rule: Always define explicit dependency arrays in every useEffect.
Why: missing dependency arrays trigger infinite render loops — these are
nearly invisible during light testing but crash real browsers under load.
Component Architecture
Directory Structure (non-negotiable):
src/
├── components/
│ ├── ui/ ← reusable: Button, Input, Modal, Card, Tooltip, Label
│ ├── auth/ ← Login, Register, PasswordRecovery forms
│ └── dashboard/ ← Sidebar, DataGrid, StatsCard, NavBar
├── styles/
│ └── global.css ← all CSS root variables live here
├── services/ ← all API call functions — never DB calls
└── app/ or pages/ ← Next.js routing onlyComponent Rules:
- Build generic UI elements in
components/ui/— they must accept props for variants (Primary/Ghost/Danger) and sizes, never hardcode a style inside. - One component, one responsibility. If a component exceeds ~150 lines, it is doing too many things — extract sub-components.
- Brand assets (logos, SVGs) go in
public/Branding/. Custom fonts go inpublic/fonts/. Never inline SVGs larger than icons.
Reusability Test: Before building a new component, ask: "Have I built
something similar before?" If yes, abstract the existing one into components/ui/
and reuse it. Duplication is the primary driver of unmaintainable frontends.
CSS Architecture
Tailwind + CSS Root Variables — always together:
Tailwind for layout, spacing, and utility classes. CSS root variables in
global.css for all brand colors, radii, and theme tokens.
/* Correct — maps to variable */
className="bg-[var(--primary)] text-[var(--foreground)]"
/* Wrong — hardcoded hex */
className="bg-[#6d28d9] text-[#111111]"Why: Hardcoded values in 50+ components require 50+ file edits for one
brand change. Variable-mapped components require editing one line in global.css.
Responsive Breakpoints — plan from the start, not as an afterthought:
sm:(640px) — mobile landscapemd:(768px) — tabletlg:(1024px) — desktopxl:(1280px) — wide desktop
Mobile Rules:
- Desktop sidebars collapse to a drawer/sheet — never inline-shrink.
- Multi-column grids stack vertically on mobile — never overflow or clip.
- Navigation becomes a bottom bar or hamburger drawer on mobile.
A4 Print Styles (for invoice/document views):
@media print {
body { background: white; margin: 0; }
.sidebar, .no-print { display: none; }
.document { width: 210mm; min-height: 297mm; padding: 20mm; }
}State Management
Local vs. Global — use this rule:
| State type | Hook / Tool | Examples |
|---|---|---|
| Isolated UI state | useState |
Dropdown open, form input, step counter |
| Shared across multiple branches | React Context | Auth session, theme preference |
| Server data / async | useEffect + useState or SWR |
API responses, user profile |
State Security Rule:
- Access tokens (JWT) → React state or Context only. Never localStorage.
- Refresh tokens → set by backend into HttpOnly cookies. Never touch from JS.
useEffect Rules — all three are mandatory:
- Always define a dependency array — empty
[]or specific deps, never omit. - Never mutate state inside a useEffect that lists that state as a dependency — this creates an infinite loop.
- Cleanup subscriptions and timers in the return function of useEffect.
State-Aware Routing:
- Authenticated user hitting
/loginor/register→ redirect to dashboard. - Unauthenticated user hitting
/dashboard→ redirect to/login. - Never render protected views before auth state is confirmed.
TypeScript Patterns
Always define interfaces for:
- API request payloads
- API response shapes
- Database document models
- Component prop types
// Correct
interface Invoice {
id: string;
userId: string;
amount: number;
status: "draft" | "sent" | "paid";
createdAt: Date;
}
// Wrong
const invoice: any = await fetchInvoice(id);Form validation: Validate locally before posting to backend. Check required fields, string formats (email), and matching values (confirm password) client-side first — this avoids unnecessary API calls and 400 errors.
Performance
Lazy Loading: Dynamically import heavy components and secondary views so they only load when needed. Why: reduces initial bundle size and time-to-interactive.
const HeavyChart = dynamic(() => import("@/components/dashboard/Chart"), {
loading: () => <Skeleton />,
});Next.js Image Component: Always use <Image /> from Next.js instead of
raw <img>. Why: automatic WebP conversion, lazy loading, and prevents layout
shifts from unspecified dimensions.
Avoid re-render triggers:
- Do not update top-level state on every keypress in a localized text input.
- Group related form fields into a single state object — not individual useState per field. Why: individual field states trigger a re-render per character typed.
Hydration Stability: Server-rendered HTML must match client-rendered output exactly. Common mismatch causes:
- Using
windoworlocalStorageduring server render — wrap inuseEffect. - Rendering timestamps or random values differently on server vs client.
Puppeteer Visual QA: For catching layout bugs across viewports, write a Puppeteer script that spins up the dev server, navigates key views, takes screenshots across Mobile/Tablet/Desktop, and feeds them back to the AI for self-correction. This is the professional alternative to manual visual testing.
API Service Layer
All backend communication must go through services/ — never call fetch
directly from a component file.
// services/invoiceService.ts
export const fetchInvoices = async (token: string) => {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/invoices`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error("Failed to fetch invoices");
return res.json();
};Why: centralizing API calls means one place to update base URLs, auth headers, and error handling — not hunting through 20 component files.
Decision Guide
| Situation | Correct action |
|---|---|
| Need same component in 2+ places | Abstract into components/ui/ first |
| State needed by sibling components | Lift to parent or use React Context |
| Component causes infinite re-render | Check useEffect dependency array first |
TypeScript throwing any errors |
Define a proper interface — do not suppress |
| Mobile layout is broken | Sidebar must be a drawer; grid must stack — fix responsive classes |
| Slow initial page load | Add dynamic imports for heavy components |
Anti-Patterns
| ❌ Never do this | ✅ Do this instead |
|---|---|
| Call DB directly from a React component | Route all data through services/ API layer |
| Store JWT in localStorage | Keep access token in React state; refresh in HttpOnly cookie |
Use any type in TypeScript |
Define interfaces for every payload and model |
| Omit useEffect dependency array | Always specify [] or exact deps |
| Hardcode hex in component className | Map to CSS root variable in global.css |
| Shrink desktop sidebar on mobile | Collapse sidebar to a drawer component |
Use raw <img> tags |
Use Next.js <Image /> with explicit dimensions |
Gotchas
Hydration errors in Next.js are almost always caused by server/client output mismatch — check for browser-only APIs (
window,document,localStorage) running during SSR. Wrap them inuseEffect.useEffectwith no dependency array runs on every render — this is almost never what you want and is a common source of infinite loops.TypeScript's
anytype is a silent failure — it compiles fine and crashes at runtime. Treat any use ofanyas a bug, not a shortcut.Grouping form fields into one state object reduces re-renders but requires spread syntax to update individual fields:
setForm(prev => ({ ...prev, email: value })).
Handoff
Skill: frontend-engineering
Type: Reference
Trigger phrases covered: "build a component", "why is my state looping",
"how should I structure this", "fix the mobile layout", "useEffect issue"
DO NOT TRIGGER for: backend routes, database schemas, server logic, deployment
Test prompts to verify:
1. "Build a reusable Button component with Primary and Ghost variants"
→ Should trigger; component goes in components/ui/, CSS vars used, cursor:pointer set
2. "My useEffect is causing an infinite loop, what's wrong?"
→ Should trigger; dependency array rules applied immediately
3. "Write a MongoDB schema for invoices"
→ Should NOT trigger; database schema is backend work
Pass/fail rubric score: 5/5
Suggested next iteration: add SWR or React Query patterns if user's projects
use server-state management heavily beyond basic useEffect fetching