- Next.js: 15.x
- TypeScript: 5.x
- React: 19.x
- Tailwind CSS: 4.x
- Node.js: 22.x LTS
- Session Management
- Clean Code
- TypeScript
- Next.js
- Tailwind CSS
- AI Development
- Security
- Testing
- Documentation
- Performance
- State Management
- Error Handling
- Deployment & DevOps
- Accessibility
- Internationalization
At the start of each session:
- Review the project's @README.md file to understand the current context
- Review open tasks in @TODO.md to identify work items
During the session:
- Mark tasks as completed in @TODO.md as they are finished
- Add notes about implementation details or decisions made
At the end of each session:
- Review @TODO.md and update task statuses
- Document any new tasks discovered
- Add any follow-up items for the next session
- Replace hard-coded values with named constants
- Use descriptive constant names that explain the value's purpose
- Keep constants at the top of the file or in a dedicated constants file
- Variables, functions, and classes should reveal their purpose
- Names should explain why something exists and how it's used
- Avoid abbreviations unless they're universally understood
- Don't comment on what the code does - make the code self-documenting
- Use comments to explain why something is done a certain way
- Document APIs, complex algorithms, and non-obvious side effects
- Each function should do exactly one thing
- Functions should be small and focused
- If a function needs a comment to explain what it does, it should be split
- Extract repeated code into reusable functions
- Share common logic through proper abstraction
- Maintain single sources of truth
- Keep related code together
- Organize code in a logical hierarchy
- Use consistent file and folder naming conventions
- Hide implementation details
- Expose clear interfaces
- Move nested conditionals into well-named functions
- Refactor continuously
- Fix technical debt early
- Leave code cleaner than you found it
- Write tests before fixing bugs
- Keep tests readable and maintainable
- Test edge cases and error conditions
- Write clear commit messages
- Make small, focused commits
- Use meaningful branch names
- Prefer interfaces over types for object definitions
- Use type for unions, intersections, and mapped types
- Avoid using
any
, preferunknown
for unknown types - Use strict TypeScript configuration
- Leverage TypeScript's built-in utility types
- Use generics for reusable type patterns
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
- Use PascalCase for type names and interfaces
- Use camelCase for variables and functions
- Use UPPER_CASE for constants
- Use descriptive names with auxiliary verbs (e.g., isLoading, hasError)
- Prefix interfaces for React props with 'Props' (e.g., ButtonProps)
- Keep type definitions close to where they're used
- Export types and interfaces from dedicated type files when shared
- Use barrel exports (index.ts) for organizing exports
- Place shared types and interfaces in their respective directories
src/types/index.ts
src/interfaces/index.ts
- Co-locate component props with their components
- Use explicit return types for public functions
- Use arrow functions for callbacks and methods
- Implement proper error handling with custom error types
- Use function overloads for complex type scenarios
- Prefer async/await over Promises
- Enable strict mode in tsconfig.json
- Use readonly for immutable properties
- Leverage discriminated unions for type safety
- Use type guards for runtime type checking
- Implement proper null checking
- Avoid type assertions unless necessary
- Create custom error types for domain-specific errors
- Use Result types for operations that can fail
- Implement proper error boundaries
- Use try-catch blocks with typed catch clauses
- Handle Promise rejections properly
- Use the Builder pattern for complex object creation
- Implement the Repository pattern for data access
- Use the Factory pattern for object creation
- Leverage dependency injection
- Use the Module pattern for encapsulation
// Example of a well-structured Next.js page
import { Metadata } from 'next'
import { Suspense } from 'react'
import { ErrorBoundary } from '@/components/error-boundary'
import { LoadingSpinner } from '@/components/ui/loading-spinner'
export const metadata: Metadata = {
title: 'Page Title',
description: 'Page description',
}
export default async function Page() {
return (
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<main className="container mx-auto px-4">
{/* Page content */}
</main>
</Suspense>
</ErrorBoundary>
)
}
// Example of proper data fetching with error handling
async function getData() {
try {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Revalidate every hour
headers: {
'Content-Type': 'application/json',
},
})
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
return res.json()
} catch (error) {
console.error('Error fetching data:', error)
throw error
}
}
// Example of proper error handling
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])
return (
<div className="flex min-h-screen flex-col items-center justify-center">
<h2 className="text-2xl font-bold">Something went wrong!</h2>
<button
className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-white"
onClick={() => reset()}
>
Try again
</button>
</div>
)
}
- Route-Specific Components: Place directly in the route folder they belong to
- Shared Feature Components: Group in feature folders within the main
components
directory - UI Components: Place generic, widely-used components in
components/ui
- Feature-First Organization: Prioritize grouping by feature over component type
- Co-location Principle: Keep related code close together
- Clear Import Paths: Use consistent import paths for better discoverability
- Use descriptive names with auxiliary verbs (isLoading, hasError)
- Prefix event handlers with "handle" (handleClick, handleSubmit)
- Use lowercase with dashes for directories (components/auth-wizard)
- For all components that are nonroutable, favor named exports for components
Routable:
export default NewsPage = () => {
return <div>News</div>;
};
Nonroutable:
const NewsFeed = () => {
return <div>News Feed</div>;
};
export { NewsFeed };
- Use Server Components by default
- Mark client components explicitly with 'use client'
- Wrap client components in Suspense with fallback
- Use dynamic loading for non-critical components
- Implement proper error boundaries
- Place static content and interfaces at end of file
- Keep data fetching logic colocated
- Leverage shadcn/ui hooks and components
- Follow Radix UI's accessibility patterns
- Implement proper state management
- Optimize images: Use WebP format, size data, lazy loading
- Minimize use of 'useEffect' and 'setState'
- Favor Server Components (RSC) where possible
- Use dynamic loading for non-critical components
- Implement proper caching strategies
- Optimize images (Avif, WebP, sizing, lazy loading)
- Implement code splitting
- Use
next/font
for font optimization - Configure
staleTimes
for client-side router cache - Monitor Core Web Vitals
- Use Server Components for data fetching when possible
- Implement proper error handling for data fetching
- Handle loading and error states appropriately
- Fetch requests are no longer cached by default
- Use appropriate caching strategies
- Use
cache: 'force-cache'
for specific cached requests unless using basehub - Implement
fetchCache = 'default-cache'
for layout/page-level caching
// Cached route handler example
export const dynamic = 'force-static'
export async function GET(request: Request) {
const params = await request.params
// Implementation
}
- Use the App Router conventions
- Implement proper loading and error states for routes
- Use dynamic routes appropriately
- Handle parallel routes when needed
- Define schemas using Zod's type-safe API
- Implement custom validation logic using Zod's
refine
method - Use Zod's inferred types for TypeScript integration
- Utilize Zod's
superRefine
for complex validation scenarios - Implement proper server-side validation
- Handle form errors appropriately
- Show loading states during form submission
- Implement proper error boundaries
- Return appropriate HTTP status codes
- Provide meaningful error messages
- Log errors appropriately
- Handle edge cases gracefully
- Use shadcn/ui alerts and toasts
- Implement proper error boundaries
- Handle API errors consistently
- Provide meaningful error messages
// app/components/errors/error-alert.tsx
import { AlertCircle } from "lucide-react";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
function ErrorAlert({ title, description }) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>{title}</AlertTitle>
<AlertDescription>{description}</AlertDescription>
</Alert>
);
}
export { ErrorAlert };
// Example of using React Query for server state
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
function TodoList() {
const queryClient = useQueryClient()
const { data: todos, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(res => res.json())
})
const mutation = useMutation({
mutationFn: (newTodo) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
})
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})
if (isLoading) return <div>Loading...</div>
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
// Example of using Zustand for client state
import create from 'zustand'
interface Store {
count: number
increment: () => void
decrement: () => void
}
const useStore = create<Store>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}))
function Counter() {
const { count, increment, decrement } = useStore()
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
)
}
// Example of using URL state with nuqs
'use client'
import { useQueryState } from 'nuqs'
function SearchFilters() {
const [search, setSearch] = useQueryState('search')
const [page, setPage] = useQueryState('page')
return (
<div>
<input
value={search || ''}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
<button onClick={() => setPage((p) => (p ? Number(p) + 1 : 1))}>
Next Page
</button>
</div>
)
}
// Example of optimized component with memo and useCallback
import { memo, useCallback } from 'react'
interface ButtonProps {
onClick: () => void
children: React.ReactNode
}
const Button = memo(function Button({ onClick, children }: ButtonProps) {
return (
<button
onClick={onClick}
className="rounded-md bg-blue-500 px-4 py-2 text-white"
>
{children}
</button>
)
})
function ParentComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked')
}, [])
return <Button onClick={handleClick}>Click me</Button>
}
// Example of optimized data fetching with SWR
import useSWR from 'swr'
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 2000,
refreshInterval: 0
})
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
return <div>Hello {data.name}!</div>
}
// Example of optimized image loading
import Image from 'next/image'
function OptimizedGallery() {
return (
<div className="grid grid-cols-3 gap-4">
{images.map((image) => (
<div key={image.id} className="relative aspect-square">
<Image
src={image.url}
alt={image.alt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover"
loading="lazy"
quality={75}
/>
</div>
))}
</div>
)
}
// Example of dynamic imports for code splitting
import dynamic from 'next/dynamic'
const DynamicChart = dynamic(() => import('@/components/Chart'), {
loading: () => <p>Loading chart...</p>,
ssr: false
})
const DynamicMap = dynamic(() => import('@/components/Map'), {
loading: () => <p>Loading map...</p>,
ssr: false
})
function Dashboard() {
return (
<div>
<DynamicChart />
<DynamicMap />
</div>
)
}
- Follow naming conventions
- Keep styles organized
- Use the latest documentation
- Follow accessibility guidelines
- Use shadcn/ui components when available
- Extend components properly
- Keep component variants consistent
- Implement proper animations
- Use proper transition utilities
- Keep accessibility in mind
- Use utility classes over custom CSS
- Group related utilities with @apply when needed
- Use proper responsive design utilities
- Implement dark mode properly
- Use proper state variants
- Keep component styles consistent
- Use Flexbox and Grid utilities effectively
- Implement proper spacing system
- Use container queries when needed
- Implement proper responsive breakpoints
- Use proper padding and margin utilities
- Implement proper alignment utilities
- Use proper font size utilities
- Implement proper line height
- Use proper font weight utilities
- Configure custom fonts properly
- Use proper text alignment
- Implement proper text decoration
- Use semantic color naming
- Implement proper color contrast
- Use opacity utilities effectively
- Configure custom colors properly
- Use proper gradient utilities
- Implement proper hover states
- Use mobile-first approach
- Implement proper breakpoints
- Use container queries effectively
- Handle different screen sizes properly
- Implement proper responsive typography
- Use proper responsive spacing
- Use proper purge configuration
- Minimize custom CSS
- Use proper caching strategies
- Implement proper code splitting
- Optimize for production
- Monitor bundle size
- Find the simplest solution possible
- When more complexity is warranted, use a workflow
- Make use of augmentation to provide context to the LLM e.g. Retrieval, Tools, Memory.
- Ensure each Agent provides an easy, well-documented interface for the LLM
- Use the correct LLM for the Agents domain
- Find the simplest solution possible
- When more complexity is warranted, use a workflow
- Use prompt chaining when tasks are easily and cleanly decomposed into fixed subtasks
- Generating Marketing copy, then translating it into a different language.
- Writing an outline of a document, checking that the outline meets certain criteria, then writing the document based on the outline.
- Use routing workflows for complex tasks
- Directing different types of customer service queries (general questions, refund requests, technical support) into different downstream processes, prompts, and tools.
- Routing easy/common questions to smaller models like Claude 3.5 Haiku and hard/unusual questions to more capable models like Claude 3.5 Sonnet to optimize cost and speed.
- Use parallelization when the divided subtasks can be parallelized for speed, or when multiple perspectives or attempts are needed for higher confidence results.
- Implementing guardrails where one model instance processes user queries while another screens them for inappropriate content or requests. This tends to perform better than having the same LLM call handle both guardrails and the core response.
- Automating evals for evaluating LLM performance, where each LLM call evaluates a different aspect of the model's performance on a given prompt.
- Reviewing a piece of code for vulnerabilities, where several different prompts review and flag the code if they find a problem.
- Evaluating whether a given piece of content is inappropriate, with multiple prompts evaluating different aspects or requiring different vote thresholds to balance false positives and negatives.
- Use orchestrator-workers for complex tasks where you can't predict the subtasks needed, where subtasks aren't pre-defined, but determined by the orchestrator based on the specific input.
- Coding products that make complex changes to multiple files each time.
- Search tasks that involve gathering and analyzing information from multiple sources for possible relevant information.
- Use evaluator-optimizer when we have clear evaluation criteria, and when iterative refinement provides measurable value.
- Literary translation where there are nuances that the translator LLM might not capture initially, but where an evaluator LLM can provide useful critiques.
- Complex search tasks that require multiple rounds of searching and analysis to gather comprehensive information, where the evaluator decides whether further searches are warranted.
- Find the simplest solution possible
- Implement proper authentication using NextAuth.js or similar
- Use role-based access control (RBAC)
- Implement proper session management
- Use secure password hashing (bcrypt)
- Implement rate limiting
- Use CSRF protection
- Implement proper CORS policies
- Encrypt sensitive data at rest
- Use environment variables for secrets
- Implement proper input validation
- Use parameterized queries to prevent SQL injection
- Implement proper XSS protection
- Use Content Security Policy (CSP)
- Implement proper file upload security
- Use proper API authentication
- Implement request validation
- Use rate limiting
- Implement proper error handling
- Use HTTPS only
- Implement proper logging
- Use API versioning
- Write tests for all business logic
- Use Jest and React Testing Library
- Mock external dependencies
- Test edge cases
- Maintain high test coverage
- Use proper test organization
- Implement proper test naming
- Test component interactions
- Test API integrations
- Test database operations
- Use proper test data
- Implement proper cleanup
- Test error scenarios
- Use proper test isolation
- Use Cypress or Playwright
- Test critical user flows
- Implement proper test data
- Test across different browsers
- Test responsive design
- Implement proper test reporting
- Use proper test organization
- Use JSDoc for function documentation
- Document complex algorithms
- Document API endpoints
- Document component props
- Document state management
- Document error handling
- Document testing strategy
- Use OpenAPI/Swagger
- Document all endpoints
- Document request/response formats
- Document error codes
- Document authentication
- Document rate limits
- Document versioning
- Maintain up-to-date README
- Document setup instructions
- Document deployment process
- Document architecture decisions
- Document coding standards
- Document testing strategy
- Document security measures
- Use GitHub Actions or similar
- Implement automated testing
- Implement automated deployment
- Use proper environment management
- Implement proper versioning
- Use proper artifact management
- Implement proper rollback strategy
- Use proper logging
- Implement error tracking
- Monitor performance metrics
- Monitor security events
- Implement proper alerting
- Use proper dashboards
- Implement proper reporting
- Use Infrastructure as Code
- Implement proper scaling
- Use proper backup strategy
- Implement proper disaster recovery
- Use proper security measures
- Implement proper monitoring
- Use proper documentation
// Example of accessible component with ARIA attributes
function AccessibleButton() {
return (
<button
aria-label="Close dialog"
aria-expanded="false"
aria-controls="dialog-content"
className="rounded-md bg-blue-500 px-4 py-2 text-white"
>
<span className="sr-only">Close</span>
<XIcon className="h-4 w-4" />
</button>
)
}
// Example of keyboard-accessible component
function AccessibleDropdown() {
const [isOpen, setIsOpen] = useState(false)
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsOpen(false)
}
}
return (
<div
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
onKeyDown={handleKeyDown}
>
<button
onClick={() => setIsOpen(!isOpen)}
aria-controls="dropdown-list"
>
Select option
</button>
{isOpen && (
<ul
id="dropdown-list"
role="listbox"
tabIndex={-1}
>
<li role="option">Option 1</li>
<li role="option">Option 2</li>
</ul>
)}
</div>
)
}
// Example of screen reader friendly component
function AccessibleForm() {
return (
<form aria-labelledby="form-title">
<h2 id="form-title">Contact Form</h2>
<div>
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
aria-required="true"
aria-invalid={false}
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={false}
/>
</div>
<button type="submit">Submit</button>
</form>
)
}
// Example of i18n setup with next-intl
import { useTranslations } from 'next-intl'
function InternationalizedComponent() {
const t = useTranslations('Common')
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('description')}</p>
<button>{t('actions.submit')}</button>
</div>
)
}
// Example of localized formatting
import { useFormatter } from 'next-intl'
function FormattedContent() {
const format = useFormatter()
return (
<div>
<p>
{format.dateTime(new Date(), {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
<p>
{format.number(1234.56, {
style: 'currency',
currency: 'USD'
})}
</p>
</div>
)
}
// Example of RTL-aware component
function RTLComponent() {
return (
<div dir="auto" className="text-right">
<p>This text will automatically align based on the content language</p>
<div className="flex flex-row-reverse">
<button>Button 1</button>
<button>Button 2</button>
</div>
</div>
)
}
// Example of language detection and switching
import { useRouter } from 'next/navigation'
import { useLocale } from 'next-intl'
function LanguageSwitcher() {
const router = useRouter()
const locale = useLocale()
const switchLanguage = (newLocale: string) => {
router.push(`/${newLocale}`)
}
return (
<div>
<button
onClick={() => switchLanguage('en')}
className={locale === 'en' ? 'font-bold' : ''}
>
English
</button>
<button
onClick={() => switchLanguage('es')}
className={locale === 'es' ? 'font-bold' : ''}
>
Español
</button>
</div>
)
}