---
description: "Modern web development: JavaScript, TypeScript, React, Vue, Next.js, Node.js, HTML/CSS, and web APIs"
globs: ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.vue", "**/*.svelte", "**/*.html", "**/*.css", "**/*.scss", "**/package.json", "**/next.config.*", "**/vite.config.*", "**/tsconfig.json"]
alwaysApply: false
---
# Web Development Patterns
Modern web development best practices for JavaScript, TypeScript, and related frameworks.
## CRITICAL: Project Setup Workflow
### ALWAYS Use CLI Tools for New Projects
**WRONG approach (creates incomplete/broken projects):**
```
1. Create package.json manually
2. Create files one by one
3. Copy component code from documentation
4. Forget to run npm install
```
**CORRECT approach:**
```bash
# Next.js
npx create-next-app@latest my-app --typescript --tailwind --app --eslint
# React (Vite)
npm create vite@latest my-app -- --template react-ts
# Vue
npm create vue@latest
# Then for component libraries:
cd my-app
npx shadcn-ui@latest init
npx shadcn-ui@latest add button card dialog
```
### Version Checking Before Setup
```
<think>
Before creating a project, I MUST:
1. Check current stable versions
2. Verify CLI options are up to date
3. Check for compatibility issues
</think>
→ web_search("create-next-app latest version options December 2024")
→ web_search("shadcn-ui Next.js 15 compatibility 2024")
```
---
## JavaScript Best Practices
### Modern Syntax
- Use `const` by default, `let` when reassignment needed, never `var`
- Prefer arrow functions for callbacks and short functions
- Use template literals for string interpolation
- Destructure objects and arrays
- Use spread operator for immutable operations
```javascript
// Modern patterns
const { name, email } = user;
const updatedList = [...items, newItem];
const greeting = `Hello, ${name}!`;
const fetchData = async () => { /* ... */ };
```
### Async Patterns
- Prefer `async/await` over `.then()` chains
- Always handle errors with try/catch
- Use `Promise.all()` for parallel operations
- Use `Promise.allSettled()` when some failures are acceptable
```javascript
// Parallel operations
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
// Error handling
try {
const data = await fetchData();
} catch (error) {
console.error('Fetch failed:', error.message);
throw new DataFetchError(error);
}
```
### Array Methods
- `map()` for transformations
- `filter()` for selection
- `reduce()` for aggregation
- `find()` for single item lookup
- Avoid mutating methods on shared data
---
## TypeScript Best Practices
### Type Definitions
- Use interfaces for object shapes
- Use types for unions, intersections, and computed types
- Prefer `unknown` over `any`
- Use `as const` for literal types
```typescript
// Interface for objects
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
// Type for unions
type Result<T> = { success: true; data: T } | { success: false; error: string };
// Generic constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
```
### Type Safety
- Enable strict mode in tsconfig
- Avoid type assertions unless necessary
- Use generics for reusable code
- Leverage inference when types are obvious
### Utility Types
```typescript
Partial<T> // All properties optional
Required<T> // All properties required
Pick<T, K> // Select specific properties
Omit<T, K> // Exclude specific properties
Record<K, V> // Object with keys K and values V
```
---
## React Patterns
### Component Structure
```tsx
// Recommended component structure
interface Props {
// Props interface at top
}
export function ComponentName({ prop1, prop2 }: Props) {
// Hooks first
const [state, setState] = useState();
const ref = useRef();
// Derived values
const computed = useMemo(() => /* ... */, [deps]);
// Effects
useEffect(() => { /* ... */ }, [deps]);
// Handlers
const handleClick = () => { /* ... */ };
// Render
return (/* JSX */);
}
```
### Hooks Best Practices
#### useState
```tsx
// Group related state
const [form, setForm] = useState({ name: '', email: '' });
// Use updater function for derived state
setCount(prev => prev + 1);
```
#### useEffect
```tsx
// Cleanup subscriptions
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
// Avoid object dependencies
useEffect(() => {
// This runs on every render if options is new object
}, [options]); // Bad
const { limit, offset } = options;
useEffect(() => {
// This only runs when primitives change
}, [limit, offset]); // Good
```
#### useMemo & useCallback
```tsx
// Memoize expensive computations
const sorted = useMemo(
() => items.sort((a, b) => a.value - b.value),
[items]
);
// Memoize callbacks passed to children
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
```
### State Management
- **Local state**: useState, useReducer
- **Lifted state**: Prop drilling for 1-2 levels
- **Context**: Theme, auth, shared global state
- **External stores**: Zustand, Redux for complex state
### Performance
- Use React.memo for expensive child components
- Virtualize long lists (react-window, react-virtuoso)
- Code split with React.lazy and Suspense
- Avoid inline object/function props
---
## Next.js Patterns (App Router)
### File Conventions
```
app/
layout.tsx # Root layout
page.tsx # Home page
loading.tsx # Loading UI
error.tsx # Error boundary
not-found.tsx # 404 page
api/ # API routes
route.ts
(group)/ # Route groups (no URL impact)
@modal/ # Parallel routes
[...slug]/ # Catch-all segments
```
### Server Components (default)
```tsx
// app/posts/page.tsx - Server Component
async function PostsPage() {
const posts = await fetchPosts(); // Direct data fetching
return <PostList posts={posts} />;
}
```
### Client Components
```tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
```
### Server Actions
```tsx
// actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title');
await db.posts.create({ title });
revalidatePath('/posts');
}
```
### Data Fetching
```tsx
// Parallel data fetching
async function Page() {
const [user, posts] = await Promise.all([
getUser(),
getPosts()
]);
}
// With caching
const data = await fetch(url, {
next: { revalidate: 3600 } // Cache for 1 hour
});
```
---
## Vue.js Patterns (Composition API)
### Component Structure
```vue
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
// Props
const props = defineProps<{
title: string;
count?: number;
}>();
// Emits
const emit = defineEmits<{
update: [value: string];
}>();
// Reactive state
const isOpen = ref(false);
const items = ref<Item[]>([]);
// Computed
const total = computed(() => items.value.length);
// Lifecycle
onMounted(() => {
fetchItems();
});
// Methods
function toggle() {
isOpen.value = !isOpen.value;
}
</script>
<template>
<div>{{ title }}</div>
</template>
```
### Composables
```typescript
// composables/useFetch.ts
export function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(true);
fetch(url)
.then(res => res.json())
.then(json => { data.value = json; })
.catch(err => { error.value = err; })
.finally(() => { loading.value = false; });
return { data, error, loading };
}
```
---
## Node.js Patterns
### Express/Fastify Structure
```typescript
// Route handler
app.get('/users/:id', async (req, res) => {
try {
const user = await userService.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
next(error); // Pass to error handler
}
});
// Error handling middleware
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ error: 'Internal server error' });
});
```
### Async Error Handling
```typescript
// Wrapper for async handlers
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/users', asyncHandler(async (req, res) => {
const users = await getUsers();
res.json(users);
}));
```
### Environment Configuration
```typescript
// config.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number).default('3000'),
DATABASE_URL: z.string().url(),
});
export const env = envSchema.parse(process.env);
```
---
## Chart.js Best Practices
### CRITICAL: Container Height Requirements
**WRONG (causes infinite expansion):**
```html
<!-- Chart will expand infinitely! -->
<div>
<canvas id="myChart"></canvas>
</div>
<script>
new Chart(ctx, {
options: {
responsive: true,
maintainAspectRatio: false // Without fixed height = BROKEN
}
});
</script>
```
**CORRECT:**
```html
<!-- Fixed height container -->
<div style="height: 400px; position: relative;">
<canvas id="myChart"></canvas>
</div>
<!-- With Tailwind -->
<div class="h-96 relative">
<canvas id="myChart"></canvas>
</div>
<script>
new Chart(ctx, {
options: {
responsive: true,
maintainAspectRatio: false // OK because container has height
}
});
</script>
```
**Alternative (maintain aspect ratio):**
```html
<div class="w-full">
<canvas id="myChart"></canvas>
</div>
<script>
new Chart(ctx, {
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 2 // Width:Height ratio
}
});
</script>
```
### Chart Initialization Checklist
```
□ Container has fixed height OR maintainAspectRatio: true
□ Canvas is inside a positioned container (position: relative)
□ Chart.js loaded before initialization
□ Canvas element exists in DOM before new Chart()
□ Chart instance stored for later updates/destroy
```
---
## Form Best Practices
### Input Validation
**WRONG (no validation):**
```html
<input type="text" placeholder="Email">
<button>Submit</button>
```
**CORRECT:**
```html
<form id="contactForm" novalidate>
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
aria-describedby="email-error"
>
<span id="email-error" class="error hidden">Please enter a valid email</span>
<button type="submit">Submit</button>
</form>
<script>
document.getElementById('contactForm').addEventListener('submit', (e) => {
e.preventDefault();
if (e.target.checkValidity()) {
// Submit form
} else {
// Show errors
e.target.classList.add('submitted');
}
});
</script>
```
### Form Accessibility Checklist
```
□ Every input has a label with matching for/id
□ Required fields marked with required attribute
□ Error messages linked with aria-describedby
□ Focus states visible (never outline: none without alternative)
□ Form has proper role if not using <form>
□ Submit button has type="submit"
```
---
## Modal/Dialog Best Practices
### Function Definition Before Use
**WRONG (ReferenceError):**
```html
<!-- Function not defined yet! -->
<button onclick="openModal('id')">Open</button>
<script>
// Defined after HTML = might not be available
function openModal(id) { /* ... */ }
</script>
```
**CORRECT:**
```html
<script>
// Define before use, or use DOMContentLoaded
function openModal(id) {
document.getElementById(id).classList.remove('hidden');
}
function closeModal(id) {
document.getElementById(id).classList.add('hidden');
}
</script>
<button onclick="openModal('myModal')">Open</button>
<div id="myModal" class="hidden">
<button onclick="closeModal('myModal')">Close</button>
</div>
```
**Even Better (Event Delegation):**
```html
<button data-modal-open="myModal">Open</button>
<div id="myModal" class="modal hidden">
<button data-modal-close>Close</button>
</div>
<script>
document.addEventListener('click', (e) => {
const openTrigger = e.target.closest('[data-modal-open]');
if (openTrigger) {
const modalId = openTrigger.dataset.modalOpen;
document.getElementById(modalId).classList.remove('hidden');
}
const closeTrigger = e.target.closest('[data-modal-close]');
if (closeTrigger) {
closeTrigger.closest('.modal').classList.add('hidden');
}
});
</script>
```
---
## CSS / Tailwind Patterns
### Tailwind Best Practices
```html
<!-- Use consistent spacing scale -->
<div class="p-4 space-y-2">
<!-- Group related utilities -->
<button class="
px-4 py-2
bg-blue-500 hover:bg-blue-600
text-white font-medium
rounded-lg
transition-colors
">
<!-- Extract components for reuse -->
<!-- Create components, don't repeat long class strings -->
```
### Responsive Design
```html
<!-- Mobile-first approach -->
<div class="
grid grid-cols-1
md:grid-cols-2
lg:grid-cols-3
gap-4
">
```
### CSS Variables for Theming
```css
:root {
--color-primary: #3b82f6;
--color-background: #ffffff;
}
.dark {
--color-primary: #60a5fa;
--color-background: #1f2937;
}
```
---
## Testing
### Unit Tests (Vitest/Jest)
```typescript
describe('calculateTotal', () => {
it('should sum all items', () => {
const items = [{ price: 10 }, { price: 20 }];
expect(calculateTotal(items)).toBe(30);
});
it('should return 0 for empty array', () => {
expect(calculateTotal([])).toBe(0);
});
});
```
### Component Tests (Testing Library)
```typescript
import { render, screen, fireEvent } from '@testing-library/react';
test('increments counter on click', async () => {
render(<Counter />);
const button = screen.getByRole('button');
await fireEvent.click(button);
expect(screen.getByText('1')).toBeInTheDocument();
});
```
### E2E Tests (Playwright)
```typescript
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
```
---
## Build Tools
### Vite Configuration
```typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': '/src',
},
},
build: {
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
});
```
### Package.json Scripts
```json
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write ."
}
}
```