← All articles

Frontend Architecture 2026: React, TypeScript, and the Stack That Scales

Frontend architecture decisions determine whether your codebase scales or becomes a liability. This guide covers the 2026 consensus stack (Next.js, Tailwind, TanStack Query, Radix UI), feature-folder structure, Server Components pattern, testing pyramid, and migration path.

Frontend architecture diagram showing feature-folder structure, Server Components boundary, and data flow

react-typescript-2026" class="internal-link">Frontend architecture decisions made today determine whether your codebase is a joy to work with or a liability in 12 months. The ecosystem has settled on clear patterns for 2026. This guide covers the architecture choices that scale, the ones that don't, and how to structure a React/TypeScript codebase that your future self will thank you for.

The 2026 Frontend Stack (Consensus Picks)

| Layer | Recommendation | Why | |-------|----------------|-----| | **Framework** | Next.js 14+ (App Router) or Astro | RSC, streaming, edge, best DX | | **Language** | TypeScript (strict mode) | Catch bugs at compile time | | **Styling** | Tailwind CSS + CSS Variables | Utility-first, design tokens, no runtime | | **State** | TanStack Query (server) + Zustand/Jotai (client) | Separation of concerns, no prop drilling | | **Forms** | React Hook Form + Zod | Type-safe, performant, accessible | | **UI Primitives** | Radix UI / cms-comparison-wordpress-sanity-strapi-2026" class="internal-link">Headless UI / shadcn/ui | Accessible, unstyled, composable | | **Animation** | Framer Motion (complex) / CSS (simple) | Performant, reducer-friendly | | **Validation** | Zod (schema) + Valibot (lighter alt) | Runtime + compile-time types | | **Testing** | Vitest + React Testing Library + Playwright | Fast unit, realistic integration | | **Lint/Format** | ESLint (typescript-eslint) + Prettier + Husky | Consistent, automated | | **Build** | Turbopack (Next.js) / Vite (Astro) | Fast HMR, optimized production | | **Deploy** | Vercel / Netlify / Cloudflare Pages | Edge, preview deploys, zero config |

---

Project Structure: Feature-Folder Architecture

src/
β”œβ”€β”€ app/                    # Next.js App Router pages (route segments)
β”‚   β”œβ”€β”€ (auth)/            # Route groups: layout-only grouping
β”‚   β”‚   β”œβ”€β”€ login/
β”‚   β”‚   └── register/
β”‚   β”œβ”€β”€ (dashboard)/
β”‚   β”‚   β”œβ”€β”€ layout.tsx     # Dashboard shell (sidebar, header)
β”‚   β”‚   β”œβ”€β”€ projects/
β”‚   β”‚   └── settings/
β”‚   β”œβ”€β”€ api/               # Route handlers (server-only)
β”‚   β”œβ”€β”€ globals.css
β”‚   β”œβ”€β”€ layout.tsx         # Root layout
β”‚   └── page.tsx           # Homepage
β”œβ”€β”€ components/            # Shared UI components (generic, reusable)
β”‚   β”œβ”€β”€ ui/                # Design system primitives (Button, Input, Dialog)
β”‚   β”‚   β”œβ”€β”€ button.tsx
β”‚   β”‚   β”œβ”€β”€ input.tsx
β”‚   β”‚   └── index.ts       # Barrel exports
β”‚   β”œβ”€β”€ forms/             # Form-specific components
β”‚   β”œβ”€β”€ layout/            # Header, Footer, Sidebar, Container
β”‚   └── providers/         # Context providers (Query, Theme, Auth)
β”œβ”€β”€ features/              # Feature-based modules (business logic)
β”‚   β”œβ”€β”€ projects/
β”‚   β”‚   β”œβ”€β”€ components/    # Project-specific components
β”‚   β”‚   β”œβ”€β”€ hooks/         # useProjects, useProjectActions
β”‚   β”‚   β”œβ”€β”€ queries/       # TanStack Query keys, hooks
β”‚   β”‚   β”œβ”€β”€ schema/        # Zod schemas
β”‚   β”‚   β”œβ”€β”€ types.ts       # TypeScript types
β”‚   β”‚   └── utils.ts       # Feature-specific utilities
β”‚   β”œβ”€β”€ auth/
β”‚   β”œβ”€β”€ billing/
β”‚   └── notifications/
β”œβ”€β”€ hooks/                 # Truly shared hooks (useMediaQuery, useLocalStorage)
β”œβ”€β”€ lib/                   # Pure utilities, no React deps
β”‚   β”œβ”€β”€ api/               # API client (Ky/Fetch wrapper)
β”‚   β”œβ”€β”€ auth/              # Token management, auth helpers
β”‚   β”œβ”€β”€ date/              # date-fns wrappers
β”‚   β”œβ”€β”€ validation/        # Zod schemas reused across features
β”‚   └── utils.ts           # cn(), formatCurrency, etc.
β”œβ”€β”€ styles/                # Global styles, Tailwind config
β”‚   β”œβ”€β”€ globals.css
β”‚   └── tokens.css         # CSS custom properties (design tokens)
β”œβ”€β”€ types/                 # Global types (API responses, env)
β”‚   β”œβ”€β”€ api.ts
β”‚   β”œβ”€β”€ user.ts
β”‚   └── global.d.ts
└── middleware.ts          # Next.js middleware (auth, i18n, logging)

**Key Principles:**

  • **Colocation**: Feature code lives together (components, hooks, types, queries)
  • **Shared only when proven**: `components/ui`, `hooks`, `lib` β€” not `components/ProjectCard` used once
  • **Route groups** `(auth)` for layout grouping without URL segment
  • **Server Components by default** β€” `'use client'` only when needed (interactivity, hooks, browser APIs)

---

Server Components First: The Mental Model

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    SERVER COMPONENT                         β”‚
β”‚  - Runs on server (build time or request time)              β”‚
β”‚  - Direct DB/API access, no bundle size cost                β”‚
β”‚  - Streams HTML β†’ client                                    β”‚
β”‚  - Cannot use: useState, useEffect, browser APIs, listeners β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    CLIENT COMPONENT                         β”‚
β”‚  - 'use client' directive                                   β”‚
β”‚  - Hydrates in browser, interactive                         β”‚
β”‚  - Can use: hooks, state, effects, browser APIs             β”‚
β”‚  - Bundle size counts                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

**Default to Server Components.** Add `'use client'` only at the *boundary* where interactivity starts.

// app/projects/page.tsx β€” Server Component (default)
import { ProjectList } from '@/features/projects/components/ProjectList';
import { getProjects } from '@/features/projects/queries/getProjects';

export default async function ProjectsPage() { // Direct data fetching β€” no API route needed const projects = await getProjects(); // async/await in Server Component

return (

Projects

); }

// features/projects/components/ProjectList.tsx β€” Client Component 'use client';

import { useState } from 'react'; import { ProjectCard } from './ProjectCard';

interface ProjectListProps { initialProjects: Project[]; }

export function ProjectList({ initialProjects }: ProjectListProps) { const [projects, setProjects] = useState(initialProjects); const [filter, setFilter] = useState('');

// Client-side filtering, sorting, pagination return (

setFilter(e.target.value)} />
{projects.filter(p => p.name.includes(filter)).map(p => ( ))}
); }

**Boundary Strategy:** Push `'use client'` down to leaves. Parent stays server, passes data as props.

---

Data Fetching: TanStack Query + Server Components

Server Components: Direct Data Access

// app/projects/[id]/page.tsx
import { ProjectDetail } from '@/features/projects/components/ProjectDetail';
import { getProject, getProjectTasks } from '@/features/projects/queries';

export default async function ProjectPage({ params }: { params: { id: string } }) { // Parallel data fetching const [project, tasks] = await Promise.all([ getProject(params.id), getProjectTasks(params.id), ]);

if (!project) notFound();

return ; }

Client Components: TanStack Query for Mutations & Client State

// features/projects/hooks/useProjectMutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';

export function useCreateProject() { const queryClient = useQueryClient();

return useMutation({ mutationFn: (data: CreateProjectInput) => api.post('/projects', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['projects'] }); }, }); }

// features/projects/components/ProjectForm.tsx 'use client';

import { useCreateProject } from '@/features/projects/hooks/useProjectMutations'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { createProjectSchema } from '@/features/projects/schema';

export function ProjectForm() { const createProject = useCreateProject(); const form = useForm({ resolver: zodResolver(createProjectSchema) });

const onSubmit = form.handleSubmit(data => createProject.mutate(data));

return (

{/* form fields */}
); }

---

State Management: Separation of Concerns

| State Type | Solution | Example | |------------|----------|---------| | **Server state** (cached, shared, sync) | TanStack Query | Projects, users, settings from API | | **Client UI state** (ephemeral, local) | `useState` / `useReducer` | Modal open, form input, tab selection | | **Global client state** (shared across features) | Zustand / Jotai | Theme, auth user, sidebar collapse | | **URL state** (shareable, bookmarkable) | Search params / Router | Filters, pagination, sort | | **Form state** | React Hook Form | Validation, submission, dirty tracking |

Global Store (Zustand) β€” Minimal, Typed

// lib/store/uiStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface UIState { theme: 'light' | 'dark' | 'system'; sidebarOpen: boolean; setTheme: (theme: UIState['theme']) => void; toggleSidebar: () => void; }

export const useUIStore = create()( persist( (set, get) => ({ theme: 'system', sidebarOpen: true, setTheme: theme => set({ theme }), toggleSidebar: () => set(state => ({ sidebarOpen: !state.sidebarOpen })), }), { name: 'ui-store' } ) );

---

Type-Safe API Layer

// lib/api/client.ts
import { z } from 'zod';

// Base API error export class APIError extends Error { constructor(public status: number, public data: unknown) { super(`API Error: ${status}`); } }

// Typed fetch wrapper async function request( endpoint: string, options: RequestInit = {}, schema?: z.ZodSchema ): Promise { const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, credentials: 'include', // cookies });

if (!res.ok) { const data = await res.json().catch(() => null); throw new APIError(res.status, data); }

if (res.status === 204) return undefined as T;

const data = await res.json(); return schema ? schema.parse(data) : data; }

// HTTP methods with Zod validation export const api = { get: (url: string, schema?: z.ZodSchema) => request(url, { method: 'GET' }, schema), post: (url: string, body: unknown, schema?: z.ZodSchema) => request(url, { method: 'POST', body: JSON.stringify(body) }, schema), patch: (url: string, body: unknown, schema?: z.ZodSchema) => request(url, { method: 'PATCH', body: JSON.stringify(body) }, schema), delete: (url: string, schema?: z.ZodSchema) => request(url, { method: 'DELETE' }, schema), };

// Usage with full type safety const project = await api.get('/projects/123', projectSchema); // ^? Project (fully typed)

---

Design System: Tokens β†’ Primitives β†’ Patterns

1. Design Tokens (CSS Custom Properties)

/* styles/tokens.css */
:root {
  /* Color */
  --color-primary-50: #eff6ff;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-foreground: #ffffff;

--color-neutral-50: #fafafa; --color-neutral-900: #171717;

/* Semantic */ --color-background: var(--color-neutral-50); --color-foreground: var(--color-neutral-900); --color-border: var(--color-neutral-200); --color-ring: var(--color-primary-500);

/* Spacing */ --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem; --space-4: 1rem; --space-6: 1.5rem; --space-8: 2rem;

/* Typography */ --font-sans: var(--font-inter), system-ui, sans-serif; --font-mono: var(--font-jetbrains-mono), monospace;

--text-xs: 0.75rem; --text-sm: 0.875rem; --text-base: 1rem; --text-lg: 1.125rem; --text-xl: 1.25rem; --text-2xl: 1.5rem; --text-3xl: 1.875rem; --text-4xl: 2.25rem;

/* Radius */ --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem;

/* Shadows */ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);

/* Transitions */ --transition-fast: 150ms ease; --transition-normal: 200ms ease; }

@media (prefers-color-scheme: dark) { :root { --color-background: var(--color-neutral-950); --color-foreground: var(--color-neutral-50); --color-border: var(--color-neutral-800); } }

2. Primitive Components (shadcn/ui pattern)

// components/ui/button.tsx
'use client';

import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils';

const buttonVariants = cva( 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', icon: 'h-10 w-10', }, }, defaultVariants: { variant: 'default', size: 'default' }, } );

export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; }

export const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button'; return ( ); } ); Button.displayName = 'Button';

3. Pattern Components (Feature-specific compositions)

// features/projects/components/ProjectCard.tsx
'use client';

import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Project } from '../types';

interface ProjectCardProps { project: Project; onEdit?: (id: string) => void; }

export function ProjectCard({ project, onEdit }: ProjectCardProps) { return (

{project.name}

{project.description}

{project.tags.map(tag => ( {tag} ))}
{onEdit && ( )}
Updated {formatDistanceToNow(new Date(project.updatedAt), { addSuffix: true })}
); }

---

Testing Strategy: Pyramid for Frontend

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   E2E (Playwright)   β”‚  ← 10%  Critical user flows
                    β”‚   5-10 tests            β”‚
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”
            β”‚       β”‚ Integration (RTL)     β”‚       β”‚  ← 20%  Component interactions
            β”‚       β”‚ 20-30 tests           β”‚       β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”
    β”‚       β”‚       β”‚ Unit (Vitest)              β”‚       β”‚  ← 70%  Pure logic, hooks, utils
    β”‚       β”‚       β”‚ 100+ tests                 β”‚       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜

Unit: Pure Logic & Hooks

// features/projects/hooks/__tests__/useProjectFilters.test.ts
import { renderHook, act } from '@testing-library/react';
import { useProjectFilters } from '../useProjectFilters';

describe('useProjectFilters', () => { it('filters projects by search term', () => { const projects = [ { id: '1', name: 'Alpha Project' }, { id: '2', name: 'Beta App' }, ];

const { result } = renderHook(() => useProjectFilters(projects));

act(() => { result.current.setSearch('alpha'); }); expect(result.current.filteredProjects).toHaveLength(1); expect(result.current.filteredProjects[0].name).toBe('Alpha Project'); }); });

Integration: Component Behavior

// features/projects/components/__tests__/ProjectForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { ProjectForm } from '../ProjectForm';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createProjectSchema } from '@/features/projects/schema';

const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} );

it('submits form and shows success', async () => { render(, { wrapper });

fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'New Project' } }); fireEvent.click(screen.getByRole('button', { name: /create/i }));

await waitFor(() => expect(screen.getByText(/created/i)).toBeInTheDocument()); });

E2E: Critical Flows

// e2e/projects.spec.ts
import { test, expect } from '@playwright/test';

test('user can create and view a project', async ({ page }) => { await page.goto('/login'); await page.fill('[name="email"]', 'test@example.com'); await page.fill('[name="password"]', 'password123'); await page.click('button:has-text("Sign in")');

await page.goto('/dashboard/projects'); await page.click('button:has-text("New Project")'); await page.fill('[name="name"]', 'Playwright Test Project'); await page.click('button:has-text("Create")');

await expect(page.locator('text=Playwright Test Project')).toBeVisible(); });

---

Performance Checklist (Enforced in CI)

// package.json scripts
{
  "scripts": {
    "lint": "eslint . --ext .ts,.tsx",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "test:e2e": "playwright test",
    "build": "next build",
    "analyze": "ANALYZE=true next build",
    "ci": "npm run lint && npm run typecheck && npm run test && npm run build"
  }
}
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:

with: { node-version: 20, cache: 'npm' }

uses: treosh/lighthouse-ci-action@v11 with: { urls: 'https://preview-url.com', budgetPath: './lighthouse-budget.json' }

  • uses: actions/checkout@v4
  • uses: actions/setup-node@v4
  • run: npm ci
  • run: npm run ci
  • name: Lighthouse CI
// lighthouse-budget.json
{
  "ci": {
    "collect": { "numberOfRuns": 3 },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["error", { "minScore": 0.95 }],
        "categories:best-practices": ["error", { "minScore": 0.9 }],
        "categories:seo": ["error", { "minScore": 0.9 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-byte-weight": ["error", { "maxNumericValue": 500000 }]
      }
    }
  }
}

---

Migration Path: From Legacy to Modern

| Legacy Pattern | Modern Replacement | Effort | |----------------|-------------------|--------| | Class components | Function + hooks | Medium | | Redux + thunks | TanStack Query + Zustand | Medium | | CSS Modules / Styled Components | Tailwind + CSS Variables | Low-Medium | | React Router (SPA) | Next.js App Router (RSC) | High | | Prop drilling | Feature folders + context/selectors | Low | | Custom form handling | React Hook Form + Zod | Low | | Manual testing | Vitest + Playwright | Medium |

**Incremental approach:**

  1. Add TypeScript + ESLint + Prettier
  2. Introduce Tailwind + design tokens
  3. Migrate leaf components to shadcn/ui primitives
  4. Add TanStack Query for server state
  5. Migrate routes to Next.js App Router (one at a time)
  6. Convert remaining class components
  7. Add test coverage for critical paths

---

The Architecture Decision Record (ADR) Template

For every significant decision, document:

# ADR-004: State Management with TanStack Query + Zustand

Status: Accepted

Context

We need to manage server state (API data) and client state (UI) in our Next.js 14 app. Redux Toolkit was considered but adds boilerplate and doesn't solve server caching.

Decision

  • Server state: TanStack Query (caching, deduping, invalidation, mutations)
  • Global client state: Zustand (theme, auth, sidebar)
  • Local UI state: useState/useReducer
  • URL state: Search params

Consequences

+ Separation of concerns, less boilerplate, great DevTools

  • Two libraries to learn (but both small APIs)
  • Team must understand server vs client state distinction

Alternatives Considered

  • Redux Toolkit: Too heavy, server caching requires RTK Query anyway
  • Jotai: Great but Zustand simpler for our global needs
  • React Context: Fine for theme, bad for high-frequency updates

---

The UI Designer Frontend Standard

Every project we build follows this architecture because:

  1. **Ships faster** β€” Feature folders = parallel work, less merge conflicts
  2. **Scales** β€” Adding features doesn't increase cognitive load
  3. **Performs** β€” Server Components + streaming + edge = fast by default
  4. **Maintains** β€” TypeScript + testing + linting = confident refactoring
  5. **Onboards** β€” New devs productive in days, not weeks

Want This Architecture for Your Project?

We scaffold this exact structure for every engagement. You get:

  • Production-ready Next.js + TypeScript + Tailwind setup
  • Design system with accessible primitives
  • API layer with Zod validation
  • Testing configuration (unit + integration + E2E)
  • CI/CD with Lighthouse budgets
  • Documentation + ADR templates

Book a free architecture review: ui-designer.in/frontend-architecture

Frequently Asked Questions

Frequently Asked Questions

Why feature folders instead of flat structure?
Feature folders (colocation) keep related code together β€” components, hooks, types, queries for a feature live in one place. This reduces cognitive load, enables parallel team work, and makes deletion/refactoring safer. Flat structures become unmanageable past ~50 components.

Frequently Asked Questions

When should I use Server Components vs Client Components?
Default to Server Components. Use Client Components (`'use client'`) only when you need: interactivity (useState, useEffect), browser APIs (localStorage, IntersectionObserver), or third-party libraries that require client execution. Push `'use client'` down to leaf components.

Frequently Asked Questions

Is Redux dead?
Not dead, but rarely needed for server state. TanStack Query handles caching, deduping, invalidation, and mutations far better. Zustand or Jotai cover global UI state with less boilerplate. Redux Toolkit + RTK Query is still valid if your team knows it well.

Frequently Asked Questions

How do I handle authentication with Server Components?
Use Next.js Middleware for route protection. Read session from cookies in Server Components via `cookies()` (App Router). For client-side auth state, use a lightweight Client Component provider that reads from a `/api/auth/me` endpoint.

Frequently Asked Questions

What's the best way to share types between frontend and backend?
Use a shared package in a monorepo (Turborepo) with tRPC for end-to-end type safety. Or generate TypeScript from OpenAPI/Swagger specs. Or use GraphQL Code Generator. Avoid manually duplicating types.

Frequently Asked Questions

How do I migrate from Redux to TanStack Query?
Incrementally. Keep Redux for UI state. Replace one API slice at a time with `useQuery`/`useMutation`. Use `queryClient.setQueryData` for optimistic updates. Remove Redux thunks/sagas as you go.

Frequently Asked Questions

Is Turborepo worth it for small teams?
Yes, if you have >2 packages (shared UI, shared config, apps). Caching, parallel execution, and independent versioning pay off quickly. For single-app projects, standard Vite/Next.js is simpler.