Article Title
Comprehensive reference for modern frontend development. Next.js 15, React 19, TypeScript, Tailwind, and best practices for scalable, performant, accessible applications.
SOLID applied to frontend development. DRY, KISS, YAGNI, SoC, LoD minimize complexity. Composition over inheritance, colocation improve maintainability.
| Principle | Description | Example | Benefits |
|---|---|---|---|
| Single Responsibility Principle | Each component should have a single reason to change. Components should do one thing well. | Separate `UserCard` (display) from `useUserData` (logic) | Easy to test, Reusable logic, Clear purpose |
| Open/Closed Principle | Components should be open for extension but closed for modification. Use composition and props. | Button with variant prop instead of ButtonPrimary, ButtonSecondary | Extensible, Prevents breaking changes, Flexible |
| Liskov Substitution Principle | Derived components should be substitutable for their parents without breaking the UI. | Custom button can replace native button seamlessly | Type-safe swaps, Polymorphism, Predictable behavior |
| Interface Segregation Principle | Clients should not depend on interfaces they do not use. Pass only needed props. | Headless component API with minimal required props | Flexibility, Fewer breaking changes, Clear contracts |
| Dependency Inversion Principle | Depend on abstractions, not concrete implementations. Inject dependencies. | useApi(client) instead of hardcoding fetch() | Testable, Pluggable, Decoupled |
| DRY (Don't Repeat Yourself) | Extract common logic into reusable hooks, components, utilities to avoid duplication. | Custom hook for form validation used across multiple forms | Single source of truth, Easy updates, Less code |
| KISS (Keep It Simple, Stupid) | Simplicity should be a primary design goal. Choose clarity over cleverness. | Explicit conditional rendering instead of complex ternary chains | Readability, Maintenance, Fewer bugs |
| YAGNI (You Aren't Gonna Need It) | Don't add functionality until it's actually needed. Avoid over-engineering. | Build pagination when users request it, not speculatively | Less code, Faster delivery, Easier pivots |
| Separation of Concerns | Keep display, logic, and data separate. UI concerns separate from business logic. | Data in custom hooks, rendering in presentational components | Testable, Maintainable, Reusable |
| Law of Demeter | Components should only communicate with their direct dependencies. Avoid deep prop drilling. | Use context for cross-cutting data instead of passing through 5 levels | Loose coupling, Flexibility, Fewer prop changes |
| Command Query Separation | Separate commands (mutations) from queries (reads). Use Server Actions for mutations. | useQuery() for reads, useServerAction() for writes | Clear intent, Caching friendly, Type-safe |
| Convention over Configuration | Establish patterns so developers don't configure basic behavior. Let conventions guide. | File-based routing, standard component folder structure | Consistency, Less boilerplate, Clear patterns |
| Composition over Inheritance | Use component composition instead of class inheritance. React favors this naturally. | Wrap Button with context provider instead of extending Button class | Flexible, No inheritance chains, Easier reasoning |
| Colocation | Place code as close as possible to where it's used. Keep related code together. | Store component CSS/utils in same folder as component | Easy discovery, Easy deletion, Self-contained |
| Single Source of Truth | Store data in one canonical place. Derived state should be computed, not stored. | Store list, compute filtered/sorted views with useMemo | No sync bugs, Easier updates, Consistent data |
| Progressive Enhancement | Build core functionality that works without JavaScript, enhance with JS. | Forms that submit to Server Actions even without JS | Resilience, Better perception, Works everywhere |
Strict mode enabled. No implicit any. Use discriminated unions for exhaustive checking. Leverage generics and utility types for reusable, type-safe abstractions.
Enable all strict type checks in tsconfig.json for maximum safety.
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}Avoid implicit any. When any is needed, use explicit types with justification.
// Bad
const data: any = fetchData();
// Good
const data: Promise = fetchData();
// Justified any
const data: any = JSON.parse(unknownString); // TODO: validate with Zod Use literal types in discriminator field to create exhaustive type checking.
type Success = { status: 'success'; data: User };
type Error = { status: 'error'; message: string };
type Result = Success | Error;
const handle = (result: Result) => {
if (result.status === 'success') {
console.log(result.data); // data is available
}
};Write generic functions and components that work with any type while preserving type safety.
function useApi(endpoint: string): Promise {
return fetch(endpoint).then(r => r.json() as T);
}
const users = await useApi('/api/users'); Use TypeScript built-in utility types: Partial, Pick, Omit, Record, Readonly, Extract, Exclude.
type UserPreview = Pick;
type UserUpdate = Partial;
type Status = 'pending' | 'success' | 'error';
type StatusRecord = Record; Let TypeScript infer types when possible. Explicit types where inference cannot work.
// Inferred
const items = [1, 2, 3]; // number[]
const config = { debug: true, maxRetries: 3 }; // inferred
// Explicit when needed
const items: (string | number)[] = [];
function parse(input: string): User { }Functional programming with pure functions and immutability. Reactive patterns for automatic state updates. Component-driven development with declarative UI.
Prefer pure functions, immutability, and function composition. Avoid side effects in rendering.
// Pure function
const filterUsers = (users: User[], role: string): User[] =>
users.filter(u => u.role === role);
// In components
const visible = useMemo(
() => filterUsers(users, selectedRole),
[users, selectedRole]
);Model UIs as streams of data changes. React to state changes automatically.
const query = useQuery(['users'], fetchUsers);
useEffect(() => {
if (query.data) {
console.log('Data updated:', query.data);
}
}, [query.data]);Build UIs as isolated, composable components. Design in Storybook first.
export function Button(props: ButtonProps) {
return ;
}
// Storybook story
export const Primary: StoryObj = {
args: { variant: 'primary', children: 'Click me' }
};Describe what the UI should be, not how to build it. Let React handle updates.
// Declarative - describe state
{loading && }
{error && }
{data &&
}
// Not imperative DOM manipulationComponents communicate through events and callbacks. Decouple with event emitters or pubsub.
const emit = useEventBus();
const handleClick = () => {
emit('item:selected', item);
};Organize components in hierarchy: atoms → molecules → organisms → templates → pages.
atoms/Button.tsx
molecules/FormField.tsx
organisms/UserForm.tsx
templates/AuthLayout.tsx
pages/LoginPage.tsxAtomic Design for hierarchy. Islands Architecture for progressive enhancement. Compound Components for flexible APIs. Composition patterns maximize reusability.
Hierarchical component organization from atoms (Button) to pages. Establishes clear naming and composition patterns.
Ship isolated, interactive components on otherwise static HTML. Reduces JS payload and enables progressive enhancement.
Components that work together with shared state. Example: Tabs with Tab, TabList, TabPanel. Flexible API.
Build complex UIs by composing simple components. Props, children, slots, and context for flexible APIs.
Separate data fetching (Container) from rendering (Presentational). Easier testing and reuse.
Component accepts function as child to customize rendering. Flexible but verbose alternative to hooks.
Proven patterns for React development. Compound Components, Custom Hooks, Render Props, HOCs, Headless Components, Provider Pattern. Hooks generally preferred over HOCs.
Components that work together with implicit shared state via context.
Extract component logic into reusable hooks. Share state and side effects.
function useUserData(userId: string) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return user;
} Pass function as prop to customize child rendering. More flexible than children.
{({ data, loading, error }) => (
<>
{loading && }
{data && }
>
)}
Function that takes component and returns enhanced component. Use sparingly, hooks preferred.
const withAuth = (Component) => (props) => {
const { user } = useAuth();
if (!user) return ;
return ;
};Unstyled, composable components that are logic-only. Style with your CSS solution.
import { Command } from 'cmdk';
Item 1
Use context providers to share state globally without prop drilling.
// In any component
const theme = useTheme();Zustand for client state. React Query v5 for server state. Jotai for atomic state. useReducer+Context for medium complexity. Clear separation of concerns.
Lightweight state management. Simple API, good DevTools, minimal boilerplate.
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// In components
const count = useStore((state) => state.count);Server state management. Handles caching, refetching, background updates, pagination.
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
});Atomic state management. Atoms as minimal units, great for complex state dependencies.
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return ;
}Built-in React API for complex state logic. No external dependency, good for medium complexity.
const [state, dispatch] = useReducer(reducer, initialState);
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
}
}File-based routing with page.tsx, layout.tsx, loading.tsx, error.tsx. Special files for conventions. Automatic code splitting and optimizations.
File convention for route segments. Exported React component renders the page UI.
// app/posts/page.tsx
export default function PostsPage() {
return Posts
;
}
// Accessible at /postsShared UI wrapping child routes. Persists across navigation, maintains state.
// app/layout.tsx
export default function RootLayout({ children }) {
return (
{children}
);
}Instant loading fallback. Suspense boundary replacement for route segments.
// app/posts/loading.tsx
export default function Loading() {
return ;
}Error boundary for route segment. Catches errors and displays fallback.
'use client';
export default function Error({ error, reset }) {
return (
<>
Something went wrong!
>
);
}Renders when notFound() is called or route doesn't exist. Segment-specific 404.
// app/posts/[id]/not-found.tsx
export default function NotFound() {
return Post not found
;
}API route handler. Exports GET, POST, PUT, DELETE functions.
// app/api/posts/route.ts
export async function GET(request: Request) {
return Response.json({ posts: [] });
}React Server Components render on server, streams to client. Server Actions for safe mutations with zero client code.
By default in app/, components run on server. Can access databases, secrets, large dependencies.
// app/posts/page.tsx - Server Component by default
import { db } from '@/lib/db';
export default async function Posts() {
const posts = await db.posts.findMany();
return ;
}Use "use client" directive for interactivity. State, hooks, browser APIs.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return ;
}Interleave server and client. Server fetches, client handles interaction.
// Server Component
export default async function Page() {
const posts = await db.posts.findMany();
return (
<>
>
);
}Async functions marked "use server". Called from Client Components securely.
'use server';
export async function createPost(data: FormData) {
const post = await db.posts.create({
title: data.get('title'),
});
revalidatePath('/posts');
return post;
}
// In Client Component
'use client';
export default function NewPost() {
return (
);
}Dynamic segments, navigation, middleware, parallel and intercepting routes for advanced routing.
[param] and [...slug] for dynamic routes.
// app/posts/[id]/page.tsx
export default function Post({ params }: { params: { id: string } }) {
return Post {params.id}
;
}
// app/docs/[...slug]/page.tsx catches /docs/a/b/c
export default function Docs({ params }: { params: { slug: string[] } }) {
return {params.slug.join('/')}
;
}<Link>, useRouter, redirect, notFound.
import Link from 'next/link';
import { useRouter } from 'next/navigation';
export default function Page() {
const router = useRouter();
return (
<>
Static Link
>
);
}middleware.ts for authentication and authorization checks.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/dashboard/:path*'],
};@folder convention for independent route segments.
// app/dashboard/@sidebar/page.tsx
// app/dashboard/@main/page.tsx
// Both render in parallel in dashboard layout(..)folder pattern to intercept child routes without navigation.
// app/photos/[id]/page.tsx intercepted by
// app/photos/(.)_[id]/modal.tsxSSR for dynamic content, SSG for static pages, ISR for hybrid approach. CSR for client-specific data. Streaming with Suspense. On-demand revalidation for cache control.
Render page on each request. Dynamic, always fresh, good for personalization.
// app/posts/page.tsx
export const dynamic = 'force-dynamic';
export default async function Posts() {
const posts = await fetch('https://api.example.com/posts', {
cache: 'no-store', // Don't cache
});
return ;
}Pre-render at build time. Fastest, best for SEO, reusable across requests.
export const revalidate = 3600; // ISR: revalidate every hour
export default async function Post() {
const post = await fetch('https://api.example.com/posts/1', {
next: { revalidate: 3600 },
});
return ;
}Regenerate static pages on-demand or on schedule. Best of SSR and SSG.
// Revalidate every 60 seconds
export const revalidate = 60;
// Or revalidate on-demand
import { revalidatePath } from 'next/cache';
export async function POST() {
revalidatePath('/posts');
return Response.json({ revalidated: true });
}Fetch in browser with useQuery. For interactive, user-specific content.
'use client';
import { useQuery } from '@tanstack/react-query';
export default function Dashboard() {
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: () => fetch('/api/user').then(r => r.json()),
});
return {user?.name};
}Stream HTML incrementally as Suspense boundaries resolve.
import { Suspense } from 'react';
export default function Page() {
return (
<>
}>
>
);
}Manually trigger cache revalidation from Server Actions or API routes.
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function publishPost(id: string) {
await db.posts.update(id, { published: true });
revalidatePath('/posts');
revalidateTag('posts');
}Tailwind v4 as default. CSS Modules for scoped styles. vanilla-extract for type-safe CSS. CVA for component variants. Custom properties for theming.
Utility-first CSS framework. Composable, performant, built-in dark mode and container queries.
Name
Description
Scoped CSS. Prevents naming collisions, explicit dependencies.
// Button.module.css
.button {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.primary {
background: blue;
}
// Button.tsx
import styles from './Button.module.css';
export function Button() {
return ;
}Type-safe CSS-in-JS. Zero-runtime, generates CSS at build time.
import { style } from '@vanilla-extract/css';
export const button = style({
padding: '0.5rem 1rem',
borderRadius: '0.25rem',
':hover': { opacity: 0.8 },
});Type-safe component API with variant composition.
import { cva } from 'class-variance-authority';
const button = cva('px-4 py-2 rounded', {
variants: {
variant: {
primary: 'bg-blue-600 text-white',
secondary: 'bg-gray-200 text-gray-900',
},
},
});
CSS variables for dynamic theming. Scoped, cascading, runtime-changeable.
/* tokens.css */
:root {
--color-primary: #0066cc;
--spacing-unit: 0.25rem;
}
[data-theme="dark"] {
--color-primary: #0088ff;
}
/* Usage */
Tailwind dark mode with class or system preference strategy.
// tailwind.config.ts
export default {
darkMode: 'class',
theme: {
extend: {},
},
};
// Usage
ContentDesign tokens, Storybook for component documentation, shadcn/ui for accessible components.
Centralized, semantic color, spacing, typography values. Single source of truth.
// tokens.ts
export const colors = {
primary: '#0066cc',
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
} as const;
export const spacing = {
xs: '0.25rem', // 4px
sm: '0.5rem', // 8px
md: '1rem', // 16px
lg: '1.5rem', // 24px
} as const;Interactive component documentation and testing. Define stories as use cases.
// Button.stories.tsx
import { Button } from './Button';
export default {
title: 'Button',
component: Button,
};
export const Primary = {
args: { variant: 'primary', children: 'Click me' },
};
export const Disabled = {
args: { disabled: true, children: 'Disabled' },
};Accessible, unstyled component library. Copy-paste components, customize freely.
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
export function MyDialog() {
return (
);
}Semantic size progression. H1-H6 and body text scales for hierarchy.
/* Typography scale */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */4px base unit for consistency. Multiply for larger scales.
/* 4px grid */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */Lucide React for consistent, scalable SVG icons.
import { Search, ChevronDown, AlertCircle } from 'lucide-react';
export function Header() {
return (
);
}Mobile-first approach with Tailwind breakpoints, container queries, fluid typography.
Mobile-first breakpoints. Use sm:, md:, lg: prefixes.
/* Default (mobile): 320px+
sm: 640px
md: 768px
lg: 1024px
xl: 1280px
2xl: 1536px
*/
Responsive text size
Responsive grid
Query parent container size instead of viewport. Better component encapsulation.
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
// In Tailwind
Component adapts to container, not viewport
Scales text smoothly between viewport sizes using clamp().
/* Font size scales from 18px at 320px to 32px at 1280px */
font-size: clamp(1.125rem, 2.5vw, 2rem);
/* Line height scales similarly */
line-height: clamp(1.5, 5vw, 1.8);
// Tailwind with clamp
className="text-[clamp(1.125rem,2.5vw,2rem)]"Optimized images with automatic format, size, and lazy loading.
import Image from 'next/image';
Maintain ratio across responsive sizes. Prevent layout shift.
// Tailwind
// CSS
.video-container {
aspect-ratio: 16 / 9;
overflow: hidden;
}Framer Motion for complex animations, View Transitions API, CSS animations, reduced motion.
Production animation library. Declarative API, springs, layouts, gestures.
import { motion } from 'framer-motion';
Content fades in and slides up
Interactive button
Smooth transitions between DOM updates without JS animation code.
'use client';
import { useRouter } from 'next/navigation';
import { startTransition } from 'react';
const router = useRouter();
const handleClick = () => {
startTransition(() => {
document.startViewTransition(() => {
router.push('/new-page');
});
});
};Native CSS for simple, performant animations. Use when no library needed.
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
/* With Tailwind */
@layer utilities {
@keyframes fadeIn { /* ... */ }
.animate-fade-in { animation: fadeIn 0.3s ease-out; }
}Respect user preference for reduced motion. Disable animations for accessibility.
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
// In Framer Motion
4-layer validation: browser, client, server, database. React Hook Form + Zod.
Type-safe form handling with client-side validation schema.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
});
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
return (
);
}Form submission to Server Actions. Server validation, revalidation.
'use server';
import { redirect } from 'next/navigation';
export async function createUser(formData: FormData) {
const email = formData.get('email');
const schema = z.object({ email: z.string().email() });
try {
const validated = schema.parse({ email });
await db.users.create(validated);
redirect('/users');
} catch (error) {
return { error: 'Invalid email' };
}
}
// In Client Component
'use client';
import { createUser } from './actions';
export function NewUserForm() {
const [error, setError] = useState('');
async function handleSubmit(formData: FormData) {
const result = await createUser(formData);
if (result?.error) setError(result.error);
}
return (
);
}Browser (HTML), client (JS), server (Action), database (constraints).
/* Layer 1: HTML */
/* Layer 2: Client Zod */
const schema = z.object({
email: z.string().email().max(100),
});
/* Layer 3: Server Action */
export async function createUser(formData: FormData) {
const validated = schema.parse(Object.fromEntries(formData));
// Server action layer
}
/* Layer 4: Database */
CREATE TABLE users (
email VARCHAR(100) NOT NULL UNIQUE,
CHECK (email ~ '^[^@]+@[^@]+$')
);Testing Trophy: 50% integration, 20% unit, 20% E2E, 8% visual, 2% perf. Vitest, Playwright, MSW.
Invest most in integration tests. Balance unit and E2E. Small visual and perf budgets.
50% Integration: Test components with dependencies
20% Unit: Test pure functions and custom hooks
20% E2E: Critical user journeys
8% Visual: Component appearance across browsers
2% Performance: Core Web Vitals targetsFast unit and integration testing. Vite-native, great DX, React testing library integration.
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with text', () => {
render();
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('handles click', async () => {
const onClick = vi.fn();
render();
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalled();
});
});E2E testing for critical user flows. Cross-browser testing, visual regression.
import { test, expect } from '@playwright/test';
test('user can login', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});Intercept network requests in tests. Consistent mocking across unit and E2E tests.
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([{ id: 1, name: 'Alice' }]);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());WCAG 2.2 AA compliance. Keyboard navigation, color contrast, ARIA, screen reader testing.
Industry standard. Minimum contrast 4.5:1 (text), all functionality keyboard accessible.
/* Contrast Checker Target */
Foreground: #000 (black)
Background: #fff (white)
Ratio: 21:1 ✓ (AAA)
Foreground: #0066cc (blue)
Background: #fff (white)
Ratio: 8.6:1 ✓ (AAA)All interactive elements accessible via Tab key. Visible focus indicators.
/* Focus indicator */
:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Tab order */
/* Skip links for keyboard users */
Skip to main content
Semantic HTML first, ARIA when needed. aria-label, aria-live, roles.
Confirm Action
Are you sure?
{message}
Test with NVDA, JAWS, VoiceOver. Verify heading structure, alt text, labels.
Header
Article Title
Trap focus in modals, restore focus on close, announce dynamic changes.
'use client';
import { useEffect, useRef } from 'react';
export function Modal({ isOpen, onClose, children }) {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen) {
const firstButton = modalRef.current?.querySelector('button');
firstButton?.focus();
}
}, [isOpen]);
return isOpen ? (
{children}
) : null;
} XSS prevention, CSRF protection, clickjacking defense, CSP, env validation, HttpOnly cookies.
Never use dangerouslySetInnerHTML. Sanitize user input with DOMPurify.
import DOMPurify from 'dompurify';
// Bad - XSS vector
// Good - sanitize
{DOMPurify.sanitize(userInput)}
// Or better - don't use HTML
{userInput}Next.js handles CSRF tokens in Server Actions automatically.
// Server Action automatically protected
'use server';
export async function updateProfile(formData: FormData) {
// CSRF token validated by Next.js
await db.users.update(userId, formData);
}Set X-Frame-Options header. Prevents embedding in iframes.
// next.config.ts
export default {
headers: async () => [
{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
],
},
],
};CSP header restricts resource loading. Prevents inline scripts.
// next.config.ts
headers: [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'nonce-{random}'; style-src 'self'",
},
],
},
],Validate env vars at startup. Use next.js .env.local for secrets.
import { z } from 'zod';
const envSchema = z.object({
NEXT_PUBLIC_API_URL: z.string().url(),
DATABASE_URL: z.string().url(),
API_SECRET: z.string().min(1),
});
const env = envSchema.parse(process.env);Auth tokens in HttpOnly cookies, inaccessible to JS. Prevents token theft.
// Server Action setting auth
'use server';
import { cookies } from 'next/headers';
export async function login(email: string, password: string) {
const token = await authenticate(email, password);
(await cookies()).set('auth', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60, // 7 days
});
}FCP <1.5s, LCP <2.5s, INP <200ms, CLS <0.1, TTFB <600ms. FBT-specific targets.
Time when largest content element renders. Target: <2.5s.
// Strategies for LCP
1. Prioritize LCP element (image/heading)
2. Optimize image: use Next/Image, modern formats
3. Defer non-critical CSS/fonts
4. Use CDN for assets
5. Preload critical resources
Responsiveness to user input. Target: <200ms.
// Reduce long tasks with useTransition
'use client';
import { useTransition } from 'react';
export function SearchResults() {
const [isPending, startTransition] = useTransition();
const handleSearch = (query: string) => {
startTransition(async () => {
await searchResults(query);
});
};
}Visual stability. Target: <0.1. Reserve space for lazy-loaded content.
}>
First pixel painted. Target: <1.5s.
// Improve FCP
1. Inline critical CSS
2. Avoid parser-blocking resources
3. Optimize fonts (system fonts preferred)
4. Minimize JS in
/* Critical styles inline */
/* Async non-critical scripts */
Server response time. Target: <600ms. Depends on hosting, caching.
// Improve TTFB
1. Use Edge runtime for low latency
2. Cache at CDN edge
3. Database optimization
4. Reduce payload
export const runtime = 'edge'; // Use Edge Runtime
export const revalidate = 3600; // Long cache TTLCode splitting, memoization, useTransition, virtual scrolling, image optimization.
Split bundles by route. Dynamic imports for heavy components.
import dynamic from 'next/dynamic';
// Split by route automatically
export default function Dashboard() {
return ;
}
// Dynamic import for heavy component
const HeavyEditor = dynamic(() => import('./Editor'), {
loading: () => ,
ssr: false, // If interactive-only
});useMemo for expensive computations, React.memo for component renders.
const expensiveValue = useMemo(
() => complexCalculation(data),
[data]
);
const MemoizedComponent = React.memo(function Item({ data }) {
return {data};
}, (prev, next) => prev.data === next.data);Defer heavy state updates. Keep UI responsive.
'use client';
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
startTransition(async () => {
await fetchLargeList(e.target.value);
});
};
return <>
{isPending && }
>;Render only visible list items. Scale to 100k+ items.
import { FixedSizeList } from 'react-window';
{({ index, style }) => (
{items[index].name}
)}
Next/Image with modern formats, responsive sizes, lazy loading.
Analyze bundle size. Remove large dependencies.
// next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
// Run: ANALYZE=true npm run buildAPI client layer, loading/error/empty states, infinite scroll, tRPC, MSW mocking.
Centralized client for all API calls. Type-safe, reusable.
// lib/api.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
export async function apiCall(
endpoint: string,
options?: RequestInit
): Promise {
const response = await fetch(`${BASE_URL}${endpoint}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!response.ok) throw new Error('API error');
return response.json();
}
// Usage
const users = await apiCall('/users'); Handle all three states in component. Clear feedback to user.
{isLoading && }
{error && }
{data?.length === 0 && }
{data &&
}Load more items on scroll. useQuery with cursor-based pagination.
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) =>
apiCall(`/posts?cursor=${pageParam}`),
getNextPageParam: (last) => last.nextCursor,
});
// Use intersection observer for infinite scroll
hasNextPage && fetchNextPage()}
/> End-to-end type safety for API. Automatic type inference.
// server/trpc.ts
export const router = t.router({
posts: {
list: t.procedure
.query(async () => db.posts.findMany()),
create: t.procedure
.input(z.object({ title: z.string() }))
.mutation(async ({ input }) => db.posts.create(input)),
},
});
// Client
const posts = await trpc.posts.list.query();Mock API responses in dev/test. Consistent across environments.
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users', () =>
HttpResponse.json([{ id: 1, name: 'Alice' }])
),
];Next.js Metadata API, dynamic OG images, sitemap, structured data JSON-LD.
Declare meta tags, OG images in page.tsx or layout.tsx.
// app/posts/[id]/page.tsx
export async function generateMetadata({ params }) {
const post = await db.posts.findUnique(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.ogImage }],
},
};
}Generate OG images at request time with Satori/Sharp.
// app/api/og/route.tsx
import { ImageResponse } from 'next/og';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get('title');
return new ImageResponse(
{title}
,
{ width: 1200, height: 630 }
);
}Generate XML sitemap for search engines.
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise {
const posts = await db.posts.findMany();
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
...posts.map(post => ({
url: `https://example.com/posts/${post.slug}`,
lastModified: post.updatedAt,
})),
];
} Add JSON-LD for rich snippets in search results.
// In page.tsx
Turbopack, ESLint, Prettier, Husky, lint-staged, Turborepo for monorepos.
Rust-based bundler. 10x faster than Webpack in next.config.ts.
// next.config.ts
export default {
experimental: {
turbopack: {
resolveAlias: {
'@': './src',
},
},
},
};Next.js ESLint config catches common issues. Extend with plugins.
// .eslintrc.json
{
"extends": "next/core-web-vitals",
"rules": {
"react/no-unescaped-entities": "off",
"@next/next/no-html-link-for-pages": "off"
}
}Code formatter. Integrated with ESLint for consistency.
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100
}Run linters on staged files before commit. Prevent bad code in repo.
// .husky/pre-commit
npx lint-staged
// .lintstagedrc
{
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.css": ["prettier --write"]
}Monorepo orchestration. Cache builds/tests across packages.
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}Vercel, Edge Runtime, environment variables, atomic deployments, caching strategies.
Push to Git, auto-deploy preview and production. Zero-config.
// vercel.json (optional)
{
"buildCommand": "npm run build",
"outputDirectory": ".next",
"env": {
"DATABASE_URL": "@database_url",
"NEXT_PUBLIC_API_URL": "@api_url"
}
}Run functions at edge. Low latency globally.
// app/api/hello/route.ts
export const runtime = 'edge';
export async function GET() {
return Response.json({ region: 'edge' });
}Separate dev, preview, production configs.
// .env.local (dev, never commit)
DATABASE_URL=postgres://localhost
// .env.production (commit, production secrets in Vercel UI)
NEXT_PUBLIC_API_URL=https://api.example.comDeploy entire version at once. No mixed versions during deploy.
// Vercel handles atomicity automatically
- Old deployment runs until new fully ready
- Switch happens instantly
- Zero downtimeCache at CDN, browser, server. Maximize hit rates.
// next.config.ts
headers: [
{
source: '/static/(.*)',
headers: [
{ key: 'Cache-Control', value: 'public, max-age=31536000' }, // 1 year
],
},
{
source: '/:path*',
headers: [
{ key: 'Cache-Control', value: 'public, max-age=3600, s-maxage=86400' },
],
},
],Non-negotiable checklist, project structure, tech stack mandates for all FBT projects.
All FBT projects must include these.
✓ TypeScript strict mode
✓ ESLint + Prettier configured
✓ React Query v5 for server state
✓ Zustand for client state
✓ Tailwind CSS v4 + shadcn/ui
✓ Next.js 15 App Router
✓ Vitest + Playwright setup
✓ Accessible components (WCAG 2.2 AA)
✓ 4-layer form validation
✓ SEO metadata configuration
✓ Error boundaries and error pages
✓ Environment variable validationStandard folder layout for consistency.
app/
├── page.tsx
├── layout.tsx
├── error.tsx
└── (auth)/
└── login/
└── page.tsx
src/
├── components/
│ ├── atoms/
│ ├── molecules/
│ └── organisms/
├── hooks/
├── lib/
│ ├── api.ts
│ └── db.ts
├── types/
└── utils/
tests/
├── unit/
└── e2e/Approved technologies. No alternatives without approval.
Runtime: Node.js 18+
Framework: Next.js 15 (App Router)
React: React 19
Language: TypeScript 5+ (strict mode)
Styling: Tailwind CSS v4
Components: shadcn/ui
State: Zustand + React Query v5
Forms: React Hook Form + Zod
Database: PostgreSQL (Prisma/Drizzle)
Auth: NextAuth.js or Clerk
Testing: Vitest + Playwright + MSW
Deployment: Vercel
Monitoring: Sentry + PostHogFBT-specific Core Web Vitals targets.
LCP: < 2.5s (yellow: 2.5-4s, red: >4s)
INP: < 200ms (yellow: 200-500ms, red: >500ms)
CLS: < 0.1 (yellow: 0.1-0.25, red: >0.25)
FCP: < 1.5s
TTFB: < 600ms
JS Bundle: < 200KB (gzipped)
Total Page Size: < 1.5MB (images included)