# State Management Standards for Next.js
This document outlines the recommended state management standards for Next.js applications. Following these guidelines will lead to more maintainable, performant, and scalable applications. These standards are tailored to contemporary Next.js development and incorporate recent features and best practices.
## 1. Introduction to State Management in Next.js
State management in Next.js refers to the strategies and tools used to handle application data, track changes, and ensure reactivity within your components. Because Next.js is a React framework, state management solutions built for React are naturally compatible. However, the unique features of Next.js, such as server-side rendering (SSR), static site generation (SSG), middleware, and the App Router, necessitate tailored approaches.
### 1.1. Why State Management Matters in Next.js
* **Data Consistency:** Enforces a single source of truth, reducing discrepancies between different components.
* **Improved Performance:** Optimizes re-renders and data fetching strategies.
* **Enhanced Maintainability:** Promotes a clear separation of concerns, simplifying debugging and code organization.
* **Scalability:** Enables predictable data flows to manage complex applications as they grow.
* **SSR/SSG Compatibility:** Ensures data is available both server-side and client-side.
### 1.2 Core Strategies
* **Component State (useState, useReducer):** Direct state management within components for local UI concerns. Suited for simple, localized state.
* **Context API:** Sharing state across components without prop drilling. Effective for theming, authentication, and other global, relatively static data.
* **Server-Side Data Fetching:** Leveraging "getServerSideProps", "getStaticProps", and now React Server Components with "async" components to fetch data on the server. This is primarily for initial page data rather than frequently changing application state.
* **Client-Side State Management Libraries (e.g., Zustand, Jotai, Recoil):** More robust solutions for complex state management needs, bringing features such as centralized stores, selectors, and optimized re-renders.
* **Caching (e.g. React Query, SWR):** Useful for server-state management. Improves performance by caching fetched data and providing features like automatic background revalidation.
* **URL State:** Storing state in the URL using "useRouter" from "next/navigation", suitable for filters, sorting, and pagination.
* **Cookies/LocalStorage (with careful consideration):** Can be used for persisting user preferences or session data, but carefully manage synchronization with server-side data.
## 2. Core Standards & Guidelines
### 2.1. Component State (useState, useReducer)
**Do This:**
* Use "useState" for simple, localized state within a single component.
* Use "useReducer" when state logic becomes more complex or involves multiple related pieces of state.
* Keep component state focused on UI-related concerns.
**Don't Do This:**
* Overuse component state for everything. Avoid prop drilling by choosing context, or dedicated state management libraries for data needed in multiple places.
* Mutate state directly. Always use the setter function provided by "useState" or dispatch actions within "useReducer".
**Why:** Component state is the simplest form of state management, but it can lead to performance issues and code duplication if overused. "useReducer" centralizes state update logic, improving maintainability.
**Example ("useState"):**
"""jsx
"use client"; // Mark as client component for interactivity
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
}
export default Counter;
"""
**Example ("useReducer"):**
"""jsx
"use client"; // Mark as client component for interactivity
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<p>Count: {state.count}</p>
dispatch({ type: 'increment' })}>Increment
dispatch({ type: 'decrement' })}>Decrement
);
}
export default Counter;
"""
### 2.2. Context API
**Do This:**
* Use Context API for sharing global state, such as theme settings, user authentication, or feature flags. It's best for data that doesn't change too often.
* Create a provider component at the root of your application or within specific subtrees.
* Use "useContext" to access the context value within consuming components.
**Don't Do This:**
* Overuse Context API for frequently updated state. Context re-renders all consuming components on any state change, which can lead to performance bottlenecks. Consider a dedicated state management library in such cases.
* Store complex state management logic directly within the context provider. Delegate complex logic to custom hooks or reducers.
**Why:** Context API avoids prop drilling, but overuse can lead to performance issues due to unnecessary re-renders. Use wisely for specific global state scenarios.
**Example:**
"""jsx
// ThemeContext.jsx
"use client"; // Mark as client component for interactivity
import React, { createContext, useState, useContext } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
const value = { theme, toggleTheme };
return (
{children}
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// Component using the context
// MyComponent.jsx
import React from 'react';
import { useTheme } from './ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return (
<p>Current Theme: {theme}</p>
Toggle Theme
);
}
export default MyComponent;
// In _app.js or layout.js
// app/layout.js
import { ThemeProvider } from './ThemeContext';
import MyComponent from './MyComponent';
export default function RootLayout({ children }) {
return (
{children}
);
}
"""
### 2.3. Server-Side Data Fetching (SSR, SSG, Server Components)
**Do This:**
* Use "getServerSideProps" (for Pages Router) or "async" Server Components (for App Router) for frequently updated data that requires SEO.
* Use "getStaticProps" (for Pages Router) for data that is fetched at build time and doesn't change often, such as blog posts or product catalogs. Ensure you use "revalidate" to periodically update the static data.
* Use Incremental Static Regeneration (ISR) with "getStaticProps" to rebuild specific pages after a defined interval without rebuilding the entire site.
* Use Server Actions for handling form submissions and mutations directly on the server.
* Use "fetch" with "cache: 'force-cache'" (default) or "cache: 'no-store'" options within Server Components to control caching behavior.
**Don't Do This:**
* Perform client-side data fetching for initial page load if server-side fetching can be used. This degrades user experience and negatively impacts SEO.
* Overuse "getServerSideProps" for static content; favor "getStaticProps" with revalidation for better performance.
* Store sensitive information directly in the URL or in client-side code.
**Why:** Server-side fetching improves SEO and initial load time by delivering pre-rendered HTML. Strategic caching optimizes data retrieval and reduces server load. Server Components move data fetching and other logic to the server, improving client-side performance.
**Example ("getServerSideProps" - Pages Router):**
"""jsx
// pages/products/[id].js
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch("https://api.example.com/products/${id}");
const product = await res.json();
return {
props: {
product,
},
};
}
function ProductPage({ product }) {
return (
{product.name}
<p>{product.description}</p>
);
}
export default ProductPage;
"""
**Example ("getStaticProps" with ISR - Pages Router):**
"""jsx
// pages/blog/[id].js
export async function getStaticPaths() {
// Call an external API endpoint to get posts
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
// Get the paths we want to pre-render based on posts
const paths = posts.map((post) => ({
params: { id: post.id },
}))
// We'll pre-render only these paths at build time.
// { fallback: 'blocking' } will server-render pages
// on-demand if the path doesn't exist.
return { paths, fallback: 'blocking' }
}
export async function getStaticProps({ params }) {
const { id } = params;
const res = await fetch("https://api.example.com/posts/${id}");
const post = await res.json();
return {
props: {
post,
},
revalidate: 60, // Revalidate every 60 seconds
};
}
function PostPage({ post }) {
return (
{post.title}
<p>{post.content}</p>
);
}
export default PostPage;
"""
**Example (Server Component with "fetch" - App Router):**
"""jsx
// app/products/[id]/page.js
async function getProduct(id) {
const res = await fetch("https://api.example.com/products/${id}", { cache: 'no-store' }); // Or 'force-cache'
const product = await res.json();
return product;
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
{product.name}
<p>{product.description}</p>
);
}
"""
**Example (Server Actions - App Router):**
"""jsx
// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
export async function addProduct(formData) {
// Simulate database call
const product = {
name: formData.get('name'),
description: formData.get('description'),
};
console.log('Adding product:', product);
// Invalidate the cache for the product list page
revalidatePath('/products');
return { message: 'Product added!' };
}
// app/products/page.js
import { addProduct } from './actions';
export default function ProductsPage() {
return (
Name:
Description:
<button type="submit">Add Product</button>
</form>
);
}
"""
### 2.4. Client-Side State Management Libraries
**Do This:**
* Select a suitable state management library (Zustand, Jotai, Recoil, Redux Toolkit) based on project needs.
* Use libraries like Zustand or Jotai for simpler global state needs and smaller bundle sizes.
* Use Recoil for complex, fine-grained state management, particularly when dealing with derived state and granular updates.
* Use Redux Toolkit for larger applications with complex state logic and where a more structured, predictable state management pattern is desired.
**Don't Do This:**
* Introduce a state management library prematurely for small applications. Start with component state or Context API when appropriate.
* Mix different state management paradigms without a clear understanding. Choose **one** primary approach for consistency.
**Why:** These libraries ensure efficient state updates, centralized management, and better organization for complex applications.
**Example (Zustand - App Router, client component):**
"""jsx
// store.js
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
export default useStore;
// components/Counter.jsx
"use client";
import React from 'react';
import useStore from '../store';
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default Counter;
"""
**Example (Jotai - App Router, client component):**
"""jsx
// atoms.js
import { atom } from 'jotai';
export const countAtom = atom(0);
// components/Counter.jsx
"use client";
import React from 'react';
import { useAtom } from 'jotai';
import { countAtom } from '../atoms';
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
export default Counter;
"""
**Example (Recoil - App Router, client component):**
"""jsx
// atoms.js
import { atom } from 'recoil';
export const countState = atom({
key: 'countState',
default: 0,
});
// components/Counter.jsx
"use client";
import React from 'react';
import { useRecoilState } from 'recoil';
import { countState } from '../atoms';
import { RecoilRoot } from 'recoil'; //Recoil needs to be wrapped
function Counter() {
const [count, setCount] = useRecoilState(countState);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
function AppWrapper() {
return (
<RecoilRoot>
<Counter />
</RecoilRoot>
);
}
export default AppWrapper;
"""
### 2.5. Caching (React Query/SWR)
**Do This:**
* Use React Query or SWR for caching server-state, especially data fetched from APIs.
* Configure appropriate cache times and revalidation strategies based on data volatility.
* Utilize features like optimistic updates and background revalidation for smoother user experiences.
**Don't Do This:**
* Manually implement caching logic when React Query or SWR can handle it more efficiently.
* Cache sensitive data without proper security measures. Handle authentication and authorization correctly.
**Why:** Caching significantly improves performance by reducing API calls, while background revalidation ensures data remains fresh.
**Example (React Query - App Router, client component):**
"""jsx
"use client";
import React from 'react';
import { useQuery } from '@tanstack/react-query';
async function fetchPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 60000, // 60 seconds
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default Posts;
"""
**Example (SWR - App Router, client component):**
"""jsx
"use client";
import React from 'react';
import useSWR from 'swr';
const fetcher = (...args) => fetch(...args).then(res => res.json());
function Posts() {
const { data, error } = useSWR('https://api.example.com/posts', fetcher, {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false
});
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default Posts;
"""
### 2.6. URL State
**Do This:**
* Use "useRouter" to read and update URL parameters for features like filtering, sorting, and pagination.
* Parse and serialize data correctly when storing complex state in the URL.
* Use "URLSearchParams" to construct and modify URL parameters efficiently.
**Don't Do This:**
* Store large amounts of data in the URL. URLs have length limits.
* Store sensitive information in the URL.
**Why:** URL state allows for bookmarkable and shareable application states.
**Example (App Router):**
"""jsx
"use client";
import React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
function ProductList() {
const router = useRouter();
const searchParams = useSearchParams();
const filter = searchParams.get('filter') || 'all';
const handleFilterChange = (newFilter) => {
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.set('filter', newFilter);
router.push("?${newSearchParams.toString()}");
};
return (
<div>
<select value={filter} onChange={(e) => handleFilterChange(e.target.value)}>
<option value="all">All</option>
<option value="available">Available</option>
<option value="discounted">Discounted</option>
</select>
{/* Product Listing based on Filter */}
</div>
);
}
export default ProductList;
"""
### 2.7. Cookies/LocalStorage
**Do This:**
* Use cookies or "localStorage" sparingly for persisting user preferences or session data.
* Use Javascript libraries like "js-cookie" for easier cookie management.
* Prefer cookies for server-side access to data using "cookies()" function in Server Components (App Router) and "req.cookies" in "getServerSideProps" (Pages Router).
* Ensure data stored is non-sensitive and consider using appropriate security flags for cookies (e.g., "httpOnly", "secure").
**Don't Do This:**
* Store large amounts of data in cookies or "localStorage". There are size limits, and it can affect performance.
* Store sensitive data like passwords or API keys.
* Rely on cookies/localStorage as the single source of truth for critical application data. They can be cleared by the user.
**Why:** Cookies and "localStorage" enable persistence across sessions, but require diligence in terms of storage capacity, security, and synchronization.
**Example (Cookies - App Router Server Component):**
"""jsx
// app/page.js (Server Component)
import { cookies } from 'next/headers';
export default function HomePage() {
const theme = cookies().get('theme')?.value || 'light';
return (
<div>
<h1>Welcome</h1>
<p>Theme: {theme}</p>
{/* ... */}
</div>
);
}
// app/actions.js (Server Action to update cookie)
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export async function setTheme(theme) {
cookies().set('theme', theme);
redirect('/');
}
"""
## 3. Anti-Patterns to Avoid
* **Prop Drilling:** Passing state down multiple levels of components that don't need it. Use Context API or a state management library instead.
* **Global Mutable State:** Directly mutating global variables, leading to unpredictable behavior. Use controlled state management with explicit updates.
* **Inconsistent Data Fetching:** Mixing server-side and client-side fetching without a clear strategy. Decide on an approach that optimizes performance and SEO.
* **Ignoring Caching Opportunities:** Not leveraging caching mechanisms in React Query or SWR, or failing to use ISR, leading to unnecessary API requests.
* **Over-Reliance on "useEffect" for Data Fetching:** Using "useEffect" in client components for initial data fetching when server-side solutions are more appropriate for the initial render.
* **Storing Sensitive Data Client-Side:** Storing sensitive information in localStorage or cookies without proper encryption or security considerations.
## 4. Performance Considerations
* **Minimize Re-renders:** Ensure components only re-render when necessary by using "React.memo", "useMemo", and "useCallback". Optimize context providers by breaking down large contexts into smaller, more specific contexts.
* **Code Splitting:** Break down your application into smaller chunks to reduce initial load time. Next.js handles this automatically with its routing system, but be mindful of large component files.
* **Optimize Data Fetching:** Use "getServerSideProps", "getStaticProps", or Server Components strategically to fetch data on the server.
* **Use Caching:** Leverage caching mechanisms provided by React Query, SWR, or by configuring "fetch" options in Server Components.
* **Lazy Load Components:** Utilize "next/dynamic" to lazily load components only when they are needed.
## 5. Conclusion
Adhering to these state management standards will contribute to building robust, scalable, and maintainable Next.js applications. By choosing the right tools and techniques for the specific needs of your project, you can optimize performance, improve developer experience, and deliver exceptional user experiences. Continuously review and adapt these standards as the Next.js ecosystem evolves.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Next.js This document outlines the component design standards for Next.js applications, focusing on creating reusable, maintainable, and performant components optimized for the Next.js ecosystem. These standards are designed to guide developers and inform AI coding assistants. ## 1. Component Architecture and Organization ### 1.1. Atomic Design Principles **Standard:** Adopt Atomic Design principles (Atoms, Molecules, Organisms, Templates, Pages) to promote reusability and modularity. * **Why:** Atomic design allows for a more systematic and scalable approach to UI development. Reusable Atoms and Molecules form the building blocks for more complex Organisms and Templates, leading to consistency and easier maintenance. **Do This:** * Categorize components based on Atomic Design principles. * Create a clear component hierarchy that reflects the UI structure. **Don't Do This:** * Create monolithic components that are difficult to reuse or maintain. * Mix different levels of abstraction within a single component. **Example:** """jsx // Atoms: Button.jsx import React from 'react'; const Button = ({ children, onClick, className = "" }) => ( <button onClick={onClick} className={"px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 ${className}"}> {children} </button> ); export default Button; // Molecules: SearchBar.jsx import React, { useState } from 'react'; import Button from './Button'; const SearchBar = ({ onSearch }) => { const [searchTerm, setSearchTerm] = useState(''); const handleSearch = () => { onSearch(searchTerm); }; return ( <div className="flex"> <input type="text" className="border rounded py-2 px-3 mr-2 focus:outline-none focus:ring-2 focus:ring-blue-400" placeholder="Search..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> <Button onClick={handleSearch}>Search</Button> </div> ); }; export default SearchBar; // Organisms : Header.jsx import React from 'react'; import SearchBar from './SearchBar'; import Logo from './Logo'; const Header = ({ onSearch }) =>( <header className="bg-gray-100 p-4 flex items-center justify-between"> <Logo /> <SearchBar onSearch={onSearch} /> </header> ); export default Header; """ ### 1.2. Component Folder Structure **Standard:** Employ a consistent folder structure for organizing components. * **Why:** A well-defined structure improves code discoverability and maintainability, especially in large projects. **Do This:** * Group related components into directories. Consider using a pattern like "components/ComponentName/ComponentName.jsx" alongside "components/ComponentName/ComponentName.module.css" or "components/ComponentName/index.jsx" to export the component. * Place shared or global components in a dedicated directory (e.g., "components/shared"). **Don't Do This:** * Scatter components randomly throughout the project. * Create excessively deep or complex directory structures. **Example:** """ src/ ├── components/ │ ├── Button/ │ │ ├── Button.jsx │ │ └── Button.module.css │ ├── Card/ │ │ ├── Card.jsx │ │ └── Card.module.css │ ├── shared/ // Global components/utilities │ │ ├── Layout.jsx │ │ └── Loader.jsx """ ### 1.3. Container vs. Presentational Components **Standard:** Differentiate between container components (handling logic and data) and presentational components (rendering UI). * **Why:** Separation of concerns makes components more reusable and testable. Container components can be easily swapped out or modified without affecting the UI. **Do This:** * Pass data and callbacks from container components to presentational components as props. * Keep presentational components focused solely on rendering the UI and avoid containing business logic. **Don't Do This:** * Mix data fetching and UI rendering within a single component. * Overcomplicate presentational components with unnecessary logic. **Example:** """jsx // Container Component: UserListContainer.jsx import React, { useState, useEffect } from 'react'; import UserList from './UserList'; const UserListContainer = () => { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchUsers = async () => { try { const response = await fetch('/api/users'); // Next.js API route const data = await response.json(); setUsers(data); setLoading(false); } catch (error) { console.error("Error fetching users:", error); setLoading(false); } }; fetchUsers(); }, []); if (loading) { return <div>Loading users...</div>; } return <UserList users={users} />; }; export default UserListContainer; // Presentational Component: UserList.jsx import React from 'react'; const UserList = ({ users }) => ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); export default UserList; """ ## 2. Component Implementation Details ### 2.1. Functional Components with Hooks **Standard:** Prefer functional components with Hooks (useState, useEffect, useContext) for managing state and side effects. Use React Server Components where applicable (data fetching, server-side rendering). * **Why:** Hooks provide a cleaner and more concise way to handle state and lifecycle functionalities compared to class components. Server Components improve performance by allowing components to render only on the server. **Do This:** * Use "useState" for managing local component state. * Use "useEffect" for performing side effects (data fetching, subscriptions). * Use "useContext" for accessing shared data from a context. * Consider React Server Components for performance-critical or data-intensive components that don't require client-side interactivity. **Don't Do This:** * Use class components unless absolutely necessary for compatibility with older libraries. * Mutate state directly; always use the state setter function provided by "useState". * Overuse "useEffect"; ensure that side effects are necessary and properly cleaned up. **Example:** """jsx // Functional Component with Hooks import React, { useState, useEffect } from 'react'; const Counter = () => { const [count, setCount] = useState(0); useEffect(() => { document.title = "Count: ${count}"; // Optional: Cleanup function (for unmounting) return () => { document.title = 'My App'; }; }, [count]); // Only re-run the effect if count changes return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }; export default Counter; // React Server Component (app directory) // app/components/DataDisplay.jsx import { getDataFromServer } from '../../lib/dataFetching'; //Dummy data function export default async function DataDisplay() { const data = await getDataFromServer(); return ( <div> <h1>Data from Server:</h1> {data.map(item => ( <p key={item.id}>{item.name}</p> ))} </div> ); } //Dummy data function // lib/dataFetching.js export async function getDataFromServer() { //Simulate fetching server data, await new Promise((resolve) => setTimeout(resolve, 1000)); const data = [ { id: 1, name: "Item A" }, { id: 2, name: "Item B" } ]; return data; } """ ### 2.2. Prop Types and Validation **Standard:** Use PropTypes or TypeScript to define and validate component props. * **Why:** Prop validation helps catch errors early during development, improving code reliability and understandability. TypeScript offers even stronger type safety. **Do This:** * Define the expected type, shape, and required status of each prop. * Use defaultProps to provide default values for optional props. * Consider using TypeScript for static type checking, especially in larger projects. **Don't Do This:** * Skip prop validation, especially for complex components or shared components. * Rely solely on runtime checks; utilize TypeScript where possible. **Example (PropTypes):** """jsx import React from 'react'; import PropTypes from 'prop-types'; const Greeting = ({ name, age, isAdult }) => ( <div> Hello, {name}! You are {age} years old. {isAdult ? 'You are an adult.' : 'You are not an adult.'} </div> ); Greeting.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number.isRequired, isAdult: PropTypes.bool }; Greeting.defaultProps = { isAdult: false } export default Greeting; """ **Example (TypeScript):** """tsx import React from 'react'; interface GreetingProps { name: string; age: number; isAdult?: boolean; // Optional prop } const Greeting: React.FC<GreetingProps> = ({ name, age, isAdult = false }) => ( <div> Hello, {name}! You are {age} years old. {isAdult ? 'You are an adult.' : 'You are not an adult.'} </div> ); export default Greeting; """ ### 2.3. Styling Approaches **Standard:** Employ a consistent styling approach (CSS Modules, Styled Components, Tailwind CSS). * **Why:** Consistent styling improves maintainability and reduces conflicts between styles. CSS Modules offer local scoping, Styled Components allow for component-level styling, and Tailwind CSS provides a utility-first approach. **Do This:** * Choose a styling approach that aligns with the project's needs and team's expertise. * Use CSS Modules for local scoping of styles. * Consider Styled Components for dynamic styles or component-specific themes. * Evaluate Tailwind CSS for rapid UI development with a utility-first approach. **Don't Do This:** * Mix multiple styling approaches within the same project without a clear strategy. * Use global CSS without proper scoping, as it can lead to conflicts. * Overuse inline styles, as they are difficult to maintain and override. **Example (CSS Modules):** """jsx // Button.jsx import React from 'react'; import styles from './Button.module.css'; const Button = ({ children, onClick }) => ( <button onClick={onClick} className={styles.button}> {children} </button> ); export default Button; // Button.module.css .button { background-color: blue; color: white; padding: 10px 20px; border-radius: 5px; cursor: pointer; } .button:hover { background-color: darkblue; } """ **Example (Tailwind CSS):** """jsx import React from 'react'; const Button = ({ children, onClick }) => ( <button onClick={onClick} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"> {children} </button> ); export default Button; """ ### 2.4. Handling Events and Callbacks **Standard:** Pass event handlers and callbacks as props to child components. * **Why:** Clear separation of concerns, making components more reusable and predictable. **Do This:** * Define event handlers in parent components that manage the logic. * Pass these handlers as props to child components. * Use arrow functions to bind "this" (if needed) or, preferably, avoid "this" altogether by using functional components. **Don't Do This:** * Define event handlers directly within child components if the logic depends on the parent component's state. * Use inline event handlers that perform complex logic. **Example:** """jsx // Parent Component: Parent.jsx import React, { useState } from 'react'; import Child from './Child'; const Parent = () => { const [message, setMessage] = useState('Hello'); const handleClick = (newMessage) => { setMessage(newMessage); }; return ( <div> <p>Message: {message}</p> <Child onClick={handleClick} /> </div> ); }; export default Parent; // Child Component: Child.jsx import React from 'react'; const Child = ({ onClick }) => ( <button onClick={() => onClick('Goodbye')}>Click Me</button> ); export default Child; """ ### 2.5 Accessibility (a11y) **Standard:** Build components with accessibility in mind, adhering to accessibility guidelines (WCAG). * **Why:** Ensures that your application is usable by everyone, including people with disabilities. Also improves SEO. **Do This:** * Use semantic HTML elements (e.g., "<button>", "<nav>", "<article>"). * Provide alternative text for images ("alt" attribute). * Ensure sufficient color contrast. * Use ARIA attributes to enhance accessibility for dynamic content. * Test components with screen readers. Tools like Axe DevTools. * Use labels with forms **Don't Do This:** * Rely solely on visual cues for conveying information. * Use generic elements (e.g., "<div>", "<span>") for interactive elements without ARIA attributes. * Neglect keyboard navigation. **Example:** """jsx import React from 'react'; const AccessibleButton = ({ children, onClick, ariaLabel }) => ( <button onClick={onClick} aria-label={ariaLabel}> {children} </button> ); export default AccessibleButton; // Example usage <AccessibleButton onClick={() => alert('Clicked!')} ariaLabel="Click to show alert"> Show Alert </AccessibleButton> //Form example <label htmlFor="name">Name:</label> <input type="text" id="name" name="name" /> """ ## 3. Next.js Specific Considerations ### 3.1. Using Next.js "Link" Component **Standard:** Use the "<Link>" component for client-side navigation between pages. * **Why:** The "<Link>" component provides client-side navigation, resulting in faster page transitions. It also Prefetches pages in the background, further improving performance. This is different from standard "<a>" tags. **Do This:** * Wrap "<a>" tags with "<Link>" when navigating between Next.js pages. * "legacyBehavior={true}" when passing custom components to "Link". **Don't Do This:** * Use regular "<a>" tags for internal navigation, as this will trigger a full page reload and negates Next.js's performance optimizations. * Omit the "href" prop. **Example:** """jsx import Link from 'next/link'; const NavLink = ({ href, children }) => ( <Link href={href}> <a>{children}</a> </Link> ); export default NavLink; // Using a custom component <Link href="/blog" legacyBehavior={true}> <CustomButton> Blog </CustomButton> </Link> """ ### 3.2. Component level Data Fetching **Standard:** Choose the appropriate data fetching strategy based on the component's needs: "getServerSideProps", "getStaticProps", or Client-Side Fetching. React Server Components are now a key consideration for data fetching. * **Why:** Next.js offers several data fetching options, each with its own trade-offs in terms of performance and data freshness. Server Components allow you to move data fetching logic to the server and reduce client-side JavaScript. **Do This:** * Use "getServerSideProps" for frequently updated data. * Use "getStaticProps" for data that doesn't change often and can be pre-rendered at build time. Use "revalidate" for incremental static regeneration. * Use client-side fetching with "useEffect" for data that requires user interaction or is specific to the client. * Utilize React Server Components for data fetching directly within components, especially when the data is not interactive. **Don't Do This:** * Overuse "getServerSideProps", as it can increase server load and slow down page load times. * Fetch all data on the client side if it can be pre-rendered. * Mix server-side and client-side data fetching within the same component without a clear reason. **Example ("getStaticProps"):** """jsx // pages/index.js import React from 'react'; export async function getStaticProps() { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await response.json(); return { props: { posts, }, revalidate: 10, // Revalidate every 10 seconds in production }; } const HomePage = ({ posts }) => ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); export default HomePage; """ **Example ("getServerSideProps"):** """jsx // pages/profile.js import React from 'react'; export async function getServerSideProps(context) { const { req, res } = context; // Access cookies or headers if needed const response = await fetch('https://api.example.com/user-profile', { headers: { cookie: req.headers.cookie, // Pass cookies to the API }, }); const profile = await response.json(); return { props: { profile, }, }; } const ProfilePage = ({ profile }) => ( <div> <h1>{profile.name}</h1> <p>{profile.email}</p> </div> ); export default ProfilePage; """ ### 3.3. Image Optimization with "next/image" **Standard:** Use the "<Image>" component from "next/image" for optimizing images. * **Why:** The "<Image>" component automatically optimizes images for different devices and screen sizes. It provides features like lazy loading and placeholder blur effects, contributing to faster page load and improved Core Web Vitals scores. **Do This:** * Replace standard "<img>" tags with "<Image>". * Provide "width" and "height" props to prevent layout shift. * Use the "layout" prop to control how the image scales. * Configure image optimization settings in "next.config.js". **Don't Do This:** * Use standard "<img>" tags for images that require optimization. * Omit "width" and "height" props, as this can cause layout shift. **Example:** """jsx import Image from 'next/image'; import myPic from '../public/me.png'; const MyImage = () => ( <Image src={myPic} alt="Picture of the author" width={500} height={500} /> ); export default MyImage; """ ### 3.4. API Routes **Standard:** Use Next.js API routes for building backend functionalities directly within the Next.js application. * **Why:** API routes provide a simple way to create serverless functions that handle API requests, without needing a separate backend server, colocated within your Next.js application. **Do This:** * Create API routes in the "pages/api" directory(or the new "app/api" directory). * Handle requests and responses using "req" and "res" objects. * Use appropriate HTTP methods (GET, POST, PUT, DELETE). * Apply proper error handling. **Don't Do This:** * Perform overly complex or long-running operations within API routes, as they have execution time limits. Consider offloading these tasks to a separate queue or background process. * Expose sensitive information or API keys directly in the code. **Example:** """javascript // pages/api/hello.js export default function handler(req, res) { res.status(200).json({ text: 'Hello' }); } """ ## 4. Performance Optimization ### 4.1. Code Splitting **Standard:** Leverages Next.js's automatic code splitting capabilities, but be mindful to avoid creating large, monolithic components. * **Why:** Code splitting reduces the initial JavaScript bundle size by splitting the application into smaller chunks, improving initial page load time. Next.js does this automatically based on route. **Do This:** * Keep individual components relatively small and focused. * Dynamically import large components or modules that are not needed on initial page load using "next/dynamic". **Don't Do This:** * Create excessively large components that include all the code for a page. * Import all dependencies at the top of the file, even if they are not used initially. **Example (Dynamic Import):** """jsx import dynamic from 'next/dynamic'; const DynamicComponent = dynamic(() => import('./MyComponent'), { loading: () => <p>Loading...</p>, }); const HomePage = () => ( <div> <p>This is the home page</p> <DynamicComponent /> </div> ); export default HomePage; """ ### 4.2. Memoization **Standard:** Use "React.memo" to memoize functional components and prevent unnecessary re-renders. * **Why:** Memoization optimizes performance by preventing components from re-rendering if their props have not changed. **Do This:** * Wrap pure functional components with "React.memo". * Use a custom comparison function as the second argument to "React.memo" if prop comparisons are more complex than simple equality. * Be cautious when using memoization with components that receive frequently changing props, as the comparison overhead may outweigh the benefits. **Don't Do This:** * Memoize components indiscriminately, as the comparison overhead can sometimes be greater than the rendering cost. * Forget to update memoization when props change. **Example:** """jsx import React from 'react'; const MyComponent = ({ name, age }) => { console.log('Rendering MyComponent'); return ( <div> <p>Name: {name}</p> <p>Age: {age}</p> </div> ); }; export default React.memo(MyComponent); """ ### 4.3. Lazy Loading **Standard:** Implement lazy loading for images and other resources that are not immediately visible on the screen. * **Why:** Lazy loading improves initial page load time by deferring the loading of non-critical resources until they are needed. **Do This:** * Use the "loading="lazy"" attribute for "<img>" tags (when not using "<Image>"). * Use a library like "react-lazyload" for lazy loading components. * Combine lazy loading with code splitting to further optimize performance. **Don't Do This:** * Lazy load above-the-fold content, as this can negatively impact the user experience. * Forget to set appropriate "width" and "height" attributes for lazy-loaded images to prevent layout shift. **Example:** """jsx import React from 'react'; import LazyLoad from 'react-lazyload'; const MyComponent = () => ( <div> <LazyLoad placeholder={<div>Loading...</div>}> <img src="image.jpg" alt="My Image" width="600" height="400" /> </LazyLoad> </div> ); export default MyComponent; """ ## 5. Security Considerations ### 5.1. Input Validation **Standard:** Validate all user inputs on both the client-side and server-side. * **Why:** Input validation prevents malicious code from being injected into the application, protecting against cross-site scripting (XSS) attacks and other vulnerabilities. **Do This:** * Use a library like "validator.js" or "yup" for client-side validation. * Implement server-side validation to ensure data integrity, even if client-side validation is bypassed. * Sanitize user inputs to remove potentially harmful characters or code. **Don't Do This:** * Rely solely on client-side validation, as it can be easily bypassed. * Trust user inputs without proper validation and sanitization. ### 5.2. Environment Variables **Standard:** Store sensitive information (API keys, database credentials) in environment variables. * **Why:** Environment variables prevent sensitive information from being exposed in the codebase, which can be a security risk. **Do This:** * Use ".env" files (or platform-specific environment variable settings) to store sensitive information. * Access environment variables using "process.env.VARIABLE_NAME". * Never commit ".env" files to version control. * Use "next.config.js" to expose environment variables to the client-side code selectively. **Don't Do This:** * Hardcode sensitive information directly in the code. * Commit ".env" files to version control. * Expose all environment variables to the client-side code, only expose what is absolutely required. ### 5.3. Cross-Site Scripting (XSS) Prevention **Standard:** Protect against XSS attacks by properly escaping user-generated content and using Content Security Policy (CSP). * **Why:** XSS attacks allow malicious users to inject scripts into the application, potentially stealing user data or performing unauthorized actions. **Do This:** * Escape user-generated content before rendering it to prevent the execution of malicious scripts, for example, using a templating engine with auto-escaping or a library designed for sanitization. * Implement a strict Content Security Policy (CSP) to control the sources from which the browser is allowed to load resources. * Use the "dangerouslySetInnerHTML" prop with caution and only for trusted content. **Don't Do This:** * Render user-generated content directly without escaping or sanitization. * Use a permissive CSP that allows scripts to be loaded from any source. * Trust user-provided HTML content without proper sanitization. By adhering to these component design standards, Next.js developers can create robust, scalable, and maintainable applications that deliver excellent performance and user experiences.
# Testing Methodologies Standards for Next.js This document outlines the testing methodologies standards for Next.js applications, providing a comprehensive guide for developers to ensure high-quality, maintainable, and reliable code. It covers unit, integration, and end-to-end testing, highlighting best practices, common anti-patterns, and specific code examples tailored for Next.js projects. ## 1. Introduction to Testing in Next.js Testing is a critical aspect of software development, especially in web applications like those built with Next.js. Thorough testing ensures that your application behaves as expected, is resilient to changes, and provides a smooth user experience. This document provides practical guidelines for implementing effective testing strategies in Next.js projects. ### 1.1 Why Testing Matters in Next.js * **Reliability:** Ensures the application functions correctly under various scenarios. * **Maintainability:** Simplifies code refactoring and updates without introducing new bugs. * **Performance:** Identifies performance bottlenecks early in the development lifecycle. * **User Experience:** Guarantees a consistent and positive user experience. * **Early Bug Detection:** Finds and fixes issues before they reach production. ### 1.2 Testing Pyramid The testing pyramid is a helpful model for determining the types and amounts of testing to perform. It suggests focusing on unit tests, with fewer integration and end-to-end tests. * **Unit Tests:** Verify individual components or functions in isolation. * **Integration Tests:** Ensure that different parts of the application work together correctly. * **End-to-End (E2E) Tests:** Simulate user interactions to validate the entire application flow. ## 2. Unit Testing Unit tests focus on individual components, functions, or modules in isolation. In Next.js, this typically means testing React components, utility functions, and API routes individually. ### 2.1 Standards for Unit Testing * **Do This:** Write unit tests for all React components, utility functions, and API route handlers. * **Don't Do This:** Skip unit tests for "simple" components or functions; all code should be tested. * **Why:** Ensures each part of the application works correctly independently, simplifying debugging and maintenance. ### 2.2 Tools and Libraries * **Jest:** A popular JavaScript testing framework with built-in support for mocking and spies. * **React Testing Library:** Encourages testing components from a user's perspective, focusing on behavior rather than implementation details. * **msw (Mock Service Worker):** Enables mocking network requests directly in the browser or Node.js environment. ### 2.3 Code Examples #### 2.3.1 Testing React Components """jsx // components/Counter.jsx import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const increment = () => { setCount(count + 1); }; const decrement = () => { setCount(count - 1); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> ); } export default Counter; """ """jsx // __tests__/Counter.test.jsx import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import Counter from '../components/Counter'; describe('Counter Component', () => { it('should render the initial count', () => { render(<Counter />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('should increment the count when the increment button is clicked', () => { render(<Counter />); fireEvent.click(screen.getByText('Increment')); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); it('should decrement the count when the decrement button is clicked', () => { render(<Counter />); fireEvent.click(screen.getByText('Decrement')); expect(screen.getByText('Count: -1')).toBeInTheDocument(); }); }); """ * **Explanation:** This example demonstrates a simple React component and its corresponding unit tests using React Testing Library. The tests verify the initial state and the behavior of the increment and decrement buttons. #### 2.3.2 Testing Utility Functions """javascript // utils/formatCurrency.js export function formatCurrency(amount, currency = 'USD') { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency, }).format(amount); } """ """javascript // __tests__/formatCurrency.test.js import { formatCurrency } from '../utils/formatCurrency'; describe('formatCurrency', () => { it('should format a number as USD currency', () => { expect(formatCurrency(1234.56)).toBe('$1,234.56'); }); it('should format a number as EUR currency', () => { expect(formatCurrency(1234.56, 'EUR')).toBe('€1,234.56'); }); it('should handle zero amounts', () => { expect(formatCurrency(0)).toBe('$0.00'); }); }); """ * **Explanation:** This code shows unit tests for a utility function that formats numbers as currency. The tests cover different currencies and edge cases like zero amounts. #### 2.3.3 Testing API Route Handlers """javascript // pages/api/hello.js export default function handler(req, res) { res.status(200).json({ message: 'Hello, world!' }); } """ """javascript // __tests__/pages/api/hello.test.js import handler from '../../pages/api/hello'; import { createMocks } from 'node-mocks-http'; describe('/api/hello', () => { it('should return a greeting message', async () => { const { req, res } = createMocks({ method: 'GET', }); await handler(req, res); expect(res._getStatusCode()).toBe(200); expect(JSON.parse(res._getData())).toEqual({ message: 'Hello, world!' }); }); }); """ * **Explanation:** This example tests an API route handler in Next.js using "node-mocks-http" to create mock requests and responses. ### 2.4 Common Anti-Patterns * **Testing Implementation Details:** Focus on testing the output and behavior of components rather than their internal implementation. Use React Testing Library to simulate user interactions. * **Skipping Edge Cases:** Neglecting to test edge cases (e.g., empty inputs, error conditions) can lead to unexpected behavior in production. * **Over-mocking:** Excessive mocking can make tests brittle and less reflective of actual application behavior. Use mocks judiciously, especially when interacting with external APIs. ### 2.5 Modern Approaches * **Component-Driven Testing:** Tools like Storybook can be integrated to write tests that verify component behavior in isolation and across various states. * **Snapshot Testing (with caution):** Useful for ensuring UI consistency, but avoid relying on them solely due to their potential for false positives. Use snapshots alongside behavioral tests. * **Mocking with MSW:** "msw" allows mocking API requests directly within your tests, making them less reliant on external API availability and more performant. ## 3. Integration Testing Integration tests verify that different parts of the application work together correctly. In Next.js, this often involves testing the interaction between components, pages, API routes, and external services. ### 3.1 Standards for Integration Testing * **Do This:** Test the interaction between components, pages, and API routes. * **Don't Do This:** Overlap with unit tests or end-to-end tests; focus on the "glue" between modules. * **Why:** Ensures that different parts of the application work together as expected, catching integration issues early. ### 3.2 Tools and Libraries * **Jest:** Can be used for integration tests, particularly for testing API routes and data fetching. * **React Testing Library:** Suitable for testing component integration and interactions. * **msw (Mock Service Worker):** Useful for mocking API requests in integration tests. * **Cypress:** A more complete testing solution that can also be used for integration tests, in addition to end-to-end tests. ### 3.3 Code Examples #### 3.3.1 Testing Component Integration """jsx // components/ProductList.jsx import React from 'react'; import ProductItem from './ProductItem'; function ProductList({ products }) { return ( <ul> {products.map((product) => ( <ProductItem key={product.id} product={product} /> ))} </ul> ); } export default ProductList; """ """jsx // components/ProductItem.jsx import React from 'react'; function ProductItem({ product }) { return ( <li> {product.name} - ${product.price} </li> ); } export default ProductItem; """ """jsx // __tests__/ProductList.test.jsx import React from 'react'; import { render, screen } from '@testing-library/react'; import ProductList from '../components/ProductList'; const mockProducts = [ { id: 1, name: 'Product 1', price: 20 }, { id: 2, name: 'Product 2', price: 30 }, ]; describe('ProductList Component', () => { it('should render a list of products', () => { render(<ProductList products={mockProducts} />); expect(screen.getByText('Product 1 - $20')).toBeInTheDocument(); expect(screen.getByText('Product 2 - $30')).toBeInTheDocument(); }); }); """ * **Explanation:** This example tests the integration between "ProductList" and "ProductItem" components. It verifies that the "ProductList" component correctly renders a list of "ProductItem" components. #### 3.3.2 Testing API Route Integration """javascript // pages/api/products.js export default async function handler(req, res) { const products = [ { id: 1, name: 'Product 1', price: 20 }, { id: 2, name: 'Product 2', price: 30 }, ]; res.status(200).json(products); } """ """jsx // components/ProductsPage.jsx import React, { useState, useEffect } from 'react'; function ProductsPage() { const [products, setProducts] = useState([]); useEffect(() => { async function fetchProducts() { const response = await fetch('/api/products'); const data = await response.json(); setProducts(data); } fetchProducts(); }, []); return ( <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> ); } export default ProductsPage; """ """jsx // __tests__/ProductsPage.test.jsx import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import ProductsPage from '../components/ProductsPage'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; const server = setupServer( rest.get('/api/products', (req, res, ctx) => { return res(ctx.json([ { id: 1, name: 'Product 1' }, { id: 2, name: 'Product 2' }, ])); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe('ProductsPage Component', () => { it('should fetch and render a list of products', async () => { render(<ProductsPage />); await waitFor(() => { expect(screen.getByText('Product 1')).toBeInTheDocument(); expect(screen.getByText('Product 2')).toBeInTheDocument(); }); }); }); """ * **Explanation:** This example integrates the "ProductsPage" component with the "/api/products" API route using "msw" to mock the API response. The test verifies that the component correctly fetches and renders the list of products. ### 3.4 Common Anti-Patterns * **Testing Through the UI Only:** Integration tests should sometimes directly test the interactions between modules behind the UI to ensure that the correct data is passed and processed. * **Ignoring Error Handling:** Ensure integration tests cover error scenarios, such as API request failures or invalid data. * **Over-Reliance on Mocks:** While necessary, excessive mocking in integration tests can lead to a false sense of security. Strive for realistic scenarios by minimizing mock usage. ### 3.5 Modern Approaches * **Contract Testing:** Define clear contracts between components and services and test against these contracts to ensure compatibility. * **Using Real Databases in Test Environments:** For more realistic integration tests, consider using a real database in a test environment (e.g., a Docker container). Tools like Testcontainers simplify this setup. * **Mocking with environment variables:** Mock endpoints based on environment variables to switch between real and mock implementations for testing purposes. ## 4. End-to-End (E2E) Testing End-to-end (E2E) tests simulate user interactions with the application to validate the entire application flow. In Next.js, this involves testing the application from the user's perspective, ensuring that all components and services work together seamlessly. ### 4.1 Standards for E2E Testing * **Do This:** Test critical user flows, such as authentication, form submissions, and data display. * **Don't Do This:** Attempt to test every single interaction; focus on the most important paths. * **Why:** Provides the highest level of confidence that the application works correctly from the user's perspective. ### 4.2 Tools and Libraries * **Cypress:** A popular end-to-end testing framework with a user-friendly interface and powerful assertions. * **Playwright:** Another powerful E2E testing framework developed by Microsoft, offering cross-browser support and advanced features. * **Puppeteer:** A Node library that provides a high-level API to control Chrome or Chromium programmatically. ### 4.3 Code Examples #### 4.3.1 Testing User Authentication """javascript // cypress/integration/auth.spec.js describe('Authentication Flow', () => { it('should successfully log in a user', () => { cy.visit('/login'); cy.get('input[name="email"]').type('test@example.com'); cy.get('input[name="password"]').type('password123'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); cy.contains('Welcome, test@example.com').should('be.visible'); }); it('should display an error message for invalid credentials', () => { cy.visit('/login'); cy.get('input[name="email"]').type('invalid@example.com'); cy.get('input[name="password"]').type('wrongpassword'); cy.get('button[type="submit"]').click(); cy.contains('Invalid credentials').should('be.visible'); }); }); """ * **Explanation:** This Cypress test suite covers the authentication flow, verifying both successful login and error handling for invalid credentials. #### 4.3.2 Testing Form Submissions """javascript // cypress/integration/contact.spec.js describe('Contact Form Submission', () => { it('should successfully submit the contact form', () => { cy.visit('/contact'); cy.get('input[name="name"]').type('John Doe'); cy.get('input[name="email"]').type('john.doe@example.com'); cy.get('textarea[name="message"]').type('This is a test message.'); cy.get('button[type="submit"]').click(); cy.contains('Thank you for your message!').should('be.visible'); }); it('should display an error message for missing fields', () => { cy.visit('/contact'); cy.get('button[type="submit"]').click(); cy.contains('Name is required').should('be.visible'); cy.contains('Email is required').should('be.visible'); cy.contains('Message is required').should('be.visible'); }); }); """ * **Explanation:** This Cypress test suite validates the contact form submission process, ensuring that the form can be submitted successfully and that error messages are displayed for missing fields. ### 4.4 Common Anti-Patterns * **Flaky Tests:** E2E tests can be prone to flakiness due to network issues, timing problems, or inconsistent test environments. Implement retry mechanisms and use explicit waits to mitigate flakiness. * **Poor Test Isolation:** Ensure each test is independent and does not rely on the state of previous tests. Use "beforeEach" and "afterEach" hooks to reset the application state between tests. * **Ignoring Accessibility:** Ensure your E2E tests also verify accessibility attributes and keyboard navigation to provide an inclusive user experience. ### 4.5 Modern Approaches * **Visual Regression Testing:** Use tools like Percy or Chromatic to detect visual changes in your application's UI. This helps prevent unintended UI regressions. * **Parallel Test Execution:** Run E2E tests in parallel to reduce test execution time. Tools like Cypress Cloud and Playwright support parallel test execution. * **Component Testing with Cypress or Playwright:** Both frameworks now support component testing, bridging the gap between unit and E2E tests and enabling more focused testing of individual components within a real browser environment. ## 5. Continuous Integration and Continuous Deployment (CI/CD) Integrating testing into your CI/CD pipeline ensures that tests are run automatically whenever code changes are made. This helps catch issues early and prevent broken code from being deployed to production. ### 5.1 Standards for CI/CD * **Do This:** Integrate unit, integration, and E2E tests into your CI/CD pipeline. * **Don't Do This:** Rely solely on manual testing before deployments; automate the testing process. * **Why:** Ensures consistent and reliable testing, reducing the risk of deploying broken code. ### 5.2 Tools and Platforms * **GitHub Actions:** A popular CI/CD platform integrated directly into GitHub. * **GitLab CI:** Another widely used CI/CD platform integrated into GitLab. * **CircleCI:** A cloud-based CI/CD platform with flexible configuration options. * **Jenkins:** An open-source automation server commonly used for CI/CD. ### 5.3 Code Examples (GitHub Actions) """yaml # .github/workflows/ci.yml name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js 18 uses: actions/setup-node@v3 with: node-version: 18 cache: 'npm' - name: Install dependencies run: npm ci - name: Run unit tests run: npm run test:unit - name: Run integration tests run: npm run test:integration - name: Run end-to-end tests uses: cypress-io/github-action@v5 with: start: npm start wait-on: 'http://localhost:3000' """ * **Explanation:** This GitHub Actions workflow defines a CI pipeline that runs unit, integration, and end-to-end tests whenever code is pushed to the "main" branch or a pull request is created. It also caches the "npm" dependencies to speed up the build process. The Cypress action is configured to start the Next.js development server and wait for it to be ready before running the tests. ### 5.4 Common Anti-Patterns * **Skipping Tests in CI/CD:** Ensure that all tests are run in the CI/CD pipeline; never skip tests to speed up deployments. * **Insufficient Test Coverage:** Monitor test coverage metrics to identify areas of the application that are not adequately tested. * **Long-Running CI/CD Pipelines:** Optimize your CI/CD pipeline to reduce build and test execution time. Parallelize tests, use caching, and optimize build configurations. ### 5.5 Modern Approaches * **Environment-Specific Configurations:** Use environment variables to configure tests for different environments (e.g., development, staging, production). * **Test Reporting:** Integrate test reporting tools to track test results and identify trends. Tools like Jest and Cypress Cloud provide detailed test reports and analytics. * **Triggering E2E tests on Preview Deployments:** Use preview deployments (e.g., on Vercel or Netlify) to run E2E tests against the deployed application before merging changes to the main branch. By following these testing methodologies standards, Next.js developers can ensure the creation of robust, maintainable, and high-quality applications. The combination of unit, integration, and end-to-end testing, along with modern tools and CI/CD integration, will lead to more reliable software and a better user experience.
# Security Best Practices Standards for Next.js This document outlines security best practices for Next.js development. Adhering to these standards will help protect your application against common vulnerabilities, ensure data integrity, and maintain user trust. ## 1. General Security Principles ### 1.1 Data Validation and Sanitization **Standard:** Always validate and sanitize user input on both the client-side (for immediate feedback) and the server-side (for security). **Why:** Prevents malicious code injection (e.g., XSS, SQL injection) and ensures data consistency. Server-side validation is crucial because client-side validation can be bypassed. **Do This:** * Use input validation libraries like "zod" or "yup" on the client-side to provide immediate feedback to users. * Implement server-side validation using similar libraries or custom validation logic. * Sanitize data before rendering it or storing it in a database. **Don't Do This:** * Trust user input without validation. * Rely solely on client-side validation. * Store or display data without sanitization. **Example:** """typescript // pages/api/submit-form.ts import { NextApiRequest, NextApiResponse } from 'next'; import { z } from 'zod'; const schema = z.object({ name: z.string().min(3).max(50), email: z.string().email(), message: z.string().min(10), }); export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === 'POST') { try { const result = schema.parse(req.body); // Process the validated data console.log('Validated Data:', result); res.status(200).json({ message: 'Form submitted successfully!' }); } catch (error) { if (error instanceof z.ZodError) { res.status(400).json({ error: error.errors }); } else { res.status(500).json({ error: 'Internal server error' }); } } } else { res.status(405).json({ error: 'Method Not Allowed' }); } } """ ### 1.2 Output Encoding **Standard:** Encode data properly when outputting it to the browser and other destinations (e.g., databases, logs). **Why:** Prevents XSS attacks by ensuring that data is treated as data, not code. Proper encoding also ensures data integrity and prevents common issues related to character sets and data corruption. **Do This:** * Use appropriate encoding techniques for different output contexts (e.g., HTML encoding for browser display, URL encoding for query parameters). * Leverage Next.js' built-in escaping mechanisms (e.g., when rendering JSX). **Don't Do This:** * Output user-provided data without encoding. * Assume that client-side frameworks automatically handle all encoding needs. **Example:** """jsx // components/DisplayMessage.tsx import React from 'react'; interface DisplayMessageProps { message: string; } const DisplayMessage: React.FC<DisplayMessageProps> = ({ message }) => { // dangerouslySetInnerHTML should be avoided if possible. In this case, // we are using it to demonstrate the importance of proper encoding. // In a real-world scenario, prefer rendering sanitized values instead. return ( <div> <p>{message}</p> </div> ); }; export default DisplayMessage; """ **Anti-Pattern:** """jsx // BAD: Vulnerable to XSS <div dangerouslySetInnerHTML={{ __html: unencodedMessage }} /> """ **Good Pattern:** """jsx // GOOD: Encoding the message with a library like DOMPurify import DOMPurify from 'dompurify'; const DisplayMessage: React.FC<DisplayMessageProps> = ({ message }) => { const cleanMessage = DOMPurify.sanitize(message); return ( <div> <p dangerouslySetInnerHTML={{ __html: cleanMessage }} /> </div> ); }; export default DisplayMessage; """ **Note:** While this examples shows sanitization, avoid "dangerouslySetInnerHTML" if at all possible and encode values directly using React instead. ### 1.3 Authentication and Authorization **Standard:** Implement robust authentication (verifying user identity) and authorization (controlling user access) mechanisms. **Why:** Prevents unauthorized access to sensitive data and functionality, ensuring that only authorized users can perform specific actions. **Do This:** * Use secure password hashing algorithms (e.g., bcrypt). * Store session tokens securely (e.g., using HTTP-only cookies). * Implement role-based access control (RBAC) to manage user permissions. * Use established authentication providers like NextAuth.js. **Don't Do This:** * Store passwords in plain text. * Use weak or predictable session tokens. * Expose sensitive data without proper authorization checks. **Example (NextAuth.js):** """typescript // pages/api/auth/[...nextauth].ts import NextAuth from 'next-auth' import GithubProvider from 'next-auth/providers/github' export const authOptions = { providers: [ GithubProvider({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, }), ], secret: process.env.NEXTAUTH_SECRET, //MUST BE SET } export default NextAuth(authOptions) """ """tsx // components/Profile.tsx import { useSession, signOut } from "next-auth/react" export default function Profile() { const { data: session } = useSession() if (session) { return ( <> Signed in as {session.user.email} <br /> <button onClick={() => signOut()}>Sign out</button> </> ) } return ( <> Not signed in <br /> <button onClick={() => signIn()}>Sign in</button> </> ) } """ ### 1.4 Secure Configuration Management **Standard:** Securely manage application configuration, especially sensitive credentials like API keys and database passwords. **Why:** Prevents unauthorized access to sensitive information that could be used to compromise the application or its data. **Do This:** * Store sensitive configuration values in environment variables. * Use a secrets management service (e.g., AWS Secrets Manager, HashiCorp Vault) for highly sensitive data. * Never commit sensitive configuration values to version control. * Explicitly define environment variables that are exposed to the browser via "NEXT_PUBLIC_". Only expose what is *necessary*. **Don't Do This:** * Hardcode sensitive configuration values in code. * Store sensitive configuration values in version control. * Expose all environment variables to the client-side. **Example (Using Environment Variables):** """typescript // next.config.js module.exports = { env: { API_ENDPOINT: process.env.API_ENDPOINT, // NEXT_PUBLIC prefix exposes this variable to the client-side NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME, }, }; // .env.local API_ENDPOINT=https://api.example.com NEXT_PUBLIC_APP_NAME=My Next.js App DATABASE_URL=postgres://user:password@host:port/database # Never checked into version control """ ### 1.5 Regular Security Updates **Standard:** Keep your Next.js dependencies and the Next.js framework itself up-to-date with the latest security patches. **Why:** Mitigates the risk of vulnerabilities being exploited in outdated libraries and frameworks. **Do This:** * Use a dependency management tool (e.g., npm, yarn, pnpm) to track and update dependencies. * Regularly run security audits on your dependencies using tools like "npm audit" or "yarn audit". * Monitor security advisories for Next.js and its dependencies. **Don't Do This:** * Ignore security alerts from dependency management tools. * Use outdated versions of Next.js or its dependencies. * Fail to update packages regularly. **Example (Running Security Audit):** """bash npm audit # or yarn audit """ ### 1.6 Error Handling and Logging **Standard:** Implement proper error handling and logging mechanisms to detect and respond to security incidents. **Why:** Provides valuable insights into potential security threats and allows for timely investigation and remediation. **Do This:** * Log significant events, such as authentication attempts, authorization failures, and application errors. * Use a centralized logging system for easier analysis and monitoring. * Implement rate limiting to prevent brute-force attacks. * Do *not* log sensitive data like passwords or API keys. **Don't Do This:** * Expose sensitive information in error messages. * Ignore or suppress errors without proper analysis. * Log sensitive data. **Example:** """typescript // pages/api/login.ts import { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { // Attempt to authenticate user const user = await authenticateUser(req.body.username, req.body.password); if (user) { // Log successful authentication console.log("User ${req.body.username} authenticated successfully."); res.status(200).json({ message: 'Login successful!' }); } else { // Log failed authentication attempt console.warn("Failed login attempt for user ${req.body.username}."); res.status(401).json({ error: 'Invalid credentials' }); } } catch (error) { // Log unexpected errors console.error('Login error:', error); res.status(500).json({ error: 'Internal server error' }); } } """ ## 2. Next.js Specific Security Considerations ### 2.1 API Routes Security **Standard:** Secure your Next.js API routes with appropriate authentication and authorization checks. **Why:** API routes are a common target for attackers, so it's crucial to ensure that only authorized users can access them. **Do This:** * Use middleware to protect API routes with authentication and authorization checks. * Validate and sanitize input parameters to prevent injection attacks. * Implement CORS (Cross-Origin Resource Sharing) to control which domains can access your API routes. **Don't Do This:** * Expose sensitive data without proper authentication. * Trust input parameters without validation. * Allow unrestricted CORS access. **Example (API Route Protection with Middleware):** """typescript // pages/api/protected-route.ts import { NextApiRequest, NextApiResponse } from 'next'; import { withAuth } from '../../middleware/auth'; async function handler(req: NextApiRequest, res: NextApiResponse) { // Your protected API logic here res.status(200).json({ message: 'This is a protected route' }); } export default withAuth(handler); // middleware/auth.ts import { NextApiRequest, NextApiResponse } from 'next'; import { getSession } from 'next-auth/react'; type NextApiHandler = (req: NextApiRequest, res: NextApiResponse) => Promise<void>; export const withAuth = (handler: NextApiHandler) => { return async (req: NextApiRequest, res: NextApiResponse) => { const session = await getSession({ req }); if (!session) { return res.status(401).json({ error: 'Unauthorized' }); } // Extend the request object with the session data (optional) (req as any).session = session; return handler(req, res); }; }; """ ### 2.2 CSRF Protection **Standard:** Implement CSRF (Cross-Site Request Forgery) protection for state-changing operations. Note: This is more relevant for traditional server-side rendered forms that are submitted. Modern APIs with "fetch" and authentication headers are typicallymitigated against CSRF attacks. **Why:** Prevents attackers from tricking users into performing actions on your application without their knowledge. **Do This:** * Use a CSRF token in forms and API requests. * Validate the CSRF token on the server-side. * Use the "same-site" cookie attribute (strict or lax) prevents the browser from sending this cookie along with cross-site requests. **Don't Do This:** * Omit CSRF protection from state-changing forms and API routes. **Example (CSRF Protection with "csrf-sync"):** """typescript // pages/api/form-submit.ts import { NextApiRequest, NextApiResponse } from 'next'; import csrf from 'csrf-sync' const { generateToken, verifyToken, getTokenName } = csrf() export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === 'POST') { const csrfTokenHeader = req.headers['csrf-token'] as string const csrfTokenBody = req.body._csrf as string; if (!csrfTokenHeader || !csrfTokenBody || csrfTokenHeader !== csrfTokenBody) { return res.status(400).json({ message: 'Invalid CSRF token' }) } try { verifyToken(csrfTokenHeader) // Process the form submission res.status(200).json({ message: 'Form submitted successfully!' }) } catch (error) { console.error("CSRF verification failed:", error) return res.status(400).json({ message: 'Invalid CSRF token' }) } } else { res.status(405).json({ message: 'Method Not Allowed' }) } } //In your form component when rendering import { useState, useEffect } from 'react'; import axios from 'axios'; import { generateCsrfToken } from '../utils/csrf'; function MyForm() { const [csrfToken, setCsrfToken] = useState(''); const [formData, setFormData] = useState({ name: '', email: '', _csrf: '' }); useEffect(() => { const fetchCsrfToken = async () => { const token = generateCsrfToken(); setCsrfToken(token); setFormData(prev => ({ ...prev, _csrf: token })); }; fetchCsrfToken(); }, []); const handleChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; const handleSubmit = async (e) => { e.preventDefault(); try { await axios.post('/api/form', formData, { headers: { 'Content-Type': 'application/json', 'CSRF-Token': csrfToken // Set CSRF token in header } }); alert('Form submitted!'); } catch (error) { console.error('Error submitting form:', error); alert('Error submitting form'); } }; } """ ### 2.3 Client-Side Security **Standard:** Minimize the amount of sensitive data exposed on the client-side. **Why:** Client-side code is visible to anyone, so it's crucial to avoid storing sensitive information or performing sensitive operations in the browser. **Do This:** * Move sensitive logic to the server-side API routes. * Avoid storing sensitive data in local storage or cookies. * Use environment variables for configuration values that are safe to expose on the client-side (prefixed with "NEXT_PUBLIC_"). **Don't Do This:** * Store API keys or secrets in client-side code. * Perform sensitive operations, such as data deletion, on the client-side. * Expose more data than necessary on client-side components. **Example (Fetching Data Safely):** """tsx // pages/profile.tsx import { useState, useEffect } from 'react'; interface UserProfile { id: number; name: string; email: string; // ... other user data (excluding sensitive fields like password) } const ProfilePage = () => { const [profile, setProfile] = useState<UserProfile | null>(null); useEffect(() => { const fetchProfile = async () => { try { const response = await fetch('/api/profile'); // Fetch via an API route. if (!response.ok) { throw new Error('Failed to fetch profile'); } const data: UserProfile = await response.json(); setProfile(data); } catch (error) { console.error('Error fetching profile:', error); } }; fetchProfile(); }, []); if (!profile) { return <p>Loading profile...</p>; } return ( <div> <h1>Profile</h1> <p>Name: {profile.name}</p> <p>Email: {profile.email}</p> </div> ); }; export default ProfilePage; // pages/api/profile.ts import { NextApiRequest, NextApiResponse } from 'next'; import { getSession } from 'next-auth/react'; //import { findUserById } from '../../lib/db'; // Your data fetching logic export default async function handler(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); if (!session) { return res.status(401).json({ error: 'Unauthorized' }); } const userId = session.user.id; // Retrieve user ID from the session //This is where you make a call to a data layer, but don't expose *all* fields, only what is needed. const user = {name: "Test", id: userId, email: "test@test.com"}; //await findUserById(userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } //Exclude sensitive fields like password, tokens, etc. const safeUser = { id: user.id, name: user.name, email: user.email }; return res.status(200).json(safeUser); // Return the safe user profile } """ ### 2.4 Third-Party Dependencies **Standard:** Carefully evaluate and manage third-party dependencies to minimize the risk of security vulnerabilities. **Why:** Third-party dependencies can introduce security vulnerabilities into your application if they are not properly maintained or if they contain malicious code. **Do This:** * Use a dependency management tool (e.g., npm, yarn, pnpm) to track and update dependencies. * Regularly run security audits on your dependencies using tools like "npm audit" or "yarn audit". * Evaluate the security posture of third-party libraries before using them. Consider factors such as the library's maintainership, community support, and known vulnerabilities. * Use [Snyk](https://snyk.io/) or similar tools to continuously monitor dependencies. * Use tools like "Renovate" to help automate dependency updates. **Don't Do This:** * Use outdated or unmaintained dependencies. * Blindly trust third-party libraries without evaluating their security. * Ignore security alerts from dependency management tools. ### 2.5 Content Security Policy (CSP) **Standard:** Implement a Content Security Policy (CSP) to mitigate the risk of XSS attacks by controlling the sources from which the browser is allowed to load resources. **Why:** CSP helps prevent attackers from injecting malicious scripts into your application by restricting the domains from which scripts, stylesheets, and other resources can be loaded. **Do This:** * Define a CSP header in your Next.js configuration or server-side code. * Use the "strict-dynamic" directive to simplify CSP configuration and improve compatibility with modern JavaScript frameworks. * Regularly review and update your CSP to ensure it's effectively protecting your application. **Don't Do This:** * Use a permissive CSP that allows resources to be loaded from any domain. * Fail to validate the CSP header on the server-side. **Example (CSP Configuration in "next.config.js"):** """javascript // next.config.js module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self';", }, ], }, ]; }, }; """ **Explanation:** * "default-src 'self'": Specifies that resources should only be loaded from the same origin as the application. * "script-src 'self' 'unsafe-eval'": Allow scripts from the same origin, and allows the use of "eval()". Removing "unsafe-eval" may break some libraries. * "style-src 'self' 'unsafe-inline'": Allow Styles from the same origin, and allows inline styles. Using a nonce hash is the best way to remove "unsafe-inline" and is the recommended standard. Tools like "styled-components" are hard to configure with CSP without the "'unsafe-inline'" declaration, but can be done. * "img-src 'self' data:": Allows loading images from the same origin and from data URLs. * "font-src 'self'": Allows loading fonts from the same origin. * "connect-src 'self'": Allows making network requests to the same origin. ### 2.6 Rate Limiting **Standard:** Implement rate limiting to protect against brute-force attacks and other forms of abuse. **Why:** Rate limiting prevents attackers from overwhelming your server with requests, which can lead to denial-of-service attacks or other security incidents. **Do This:** * Use middleware to implement rate limiting for API routes and other critical endpoints. * Configure rate limits based on factors such as IP address, user ID, and request type. * Implement appropriate error handling to inform users when they have exceeded the rate limit. **Don't Do This:** * Fail to implement rate limiting for critical endpoints. * Use overly permissive rate limits that allow attackers to make a large number of requests. **Example (Rate Limiting with "next-rate-limit"):** """typescript // pages/api/protected.ts import { NextApiRequest, NextApiResponse } from 'next'; import { RateLimit } from 'next-rate-limit'; const rateLimit = RateLimit({ interval: 60 * 1000, // 1 minute rate: 10, // 10 requests per minute uniqueTokenPerInterval: 500, // Max 500 users per minute }); async function handler(req: NextApiRequest, res: NextApiResponse) { try { await rateLimit.consume(req, res); res.status(200).json({ message: 'API Route accessed successfully' }); } catch (e) { console.info(e); res.status(429).json({ message: 'Too Many Requests' }); } } export default handler; """ ### 2.7 Preventing Clickjacking **Standard:** Implement defenses against clickjacking attacks by setting the "X-Frame-Options" header. **Why:** Clickjacking tricks users into clicking something different from what they perceive, potentially leading to malicious actions. The "X-Frame-Options" header controls whether your site can be embedded in a "<frame>", "<iframe>", or "<object>". **Do This:** * Set the "X-Frame-Options" header to "DENY" to prevent your site from being framed at all, or "SAMEORIGIN" to allow framing only by pages from the same origin. **"DENY" is generally the recommended setting**. **Don't Do This:** * Leave the "X-Frame-Options" header unset, which leaves your site vulnerable to clickjacking. Don't set it to "ALLOW-FROM", as this directive is deprecated and not supported by all browsers. **Example (Setting "X-Frame-Options" in "next.config.js"):** """javascript module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'X-Frame-Options', value: 'DENY', // Or 'SAMEORIGIN', but DENY is preferred }, ], }, ]; }, }; """ ## 3. Ongoing Security Practices ### 3.1 Penetration Testing **Standard:** Conduct regular penetration testing to identify vulnerabilities in your application. **Why:** Penetration testing simulates real-world attacks, allowing you to discover and address security weaknesses before they can be exploited by malicious actors. **Do This:** * Engage qualified security professionals to perform penetration testing on your application. * Conduct penetration testing at least annually, or more frequently if your application undergoes significant changes. * Address all identified vulnerabilities promptly and thoroughly. ### 3.2 Security Training **Standard:** Provide regular security training to your development team. **Why:** Security training helps developers build more secure applications by raising awareness of common vulnerabilities and secure coding practices. **Do This:** * Provide regular training on topics such as OWASP Top 10, secure coding practices, and data privacy. * Encourage developers to stay up-to-date on the latest security threats and trends. By adhering to these security best practices, you can significantly reduce the risk of vulnerabilities in your Next.js application and protect your users and data. This guide should be considered a living document and be updated regularly to reflect the evolving threat landscape and best practices.
# Tooling and Ecosystem Standards for Next.js This document outlines the coding standards specifically related to tooling and the ecosystem when developing Next.js applications. Adhering to these standards will improve code quality, maintainability, performance, and security. ## 1. Development Environment Setup ### 1.1 Node.js Version Management **Standard:** Use Node.js version specified in the ".nvmrc" file. Stay up-to-date with LTS versions. **Do This:** * Use "nvm" (Node Version Manager) or "asdf" for managing Node.js versions. * Include an ".nvmrc" file in your project root to specify the required Node.js version (e.g., "18.x" or "20.x"). **Don't Do This:** * Rely on a globally installed Node.js version without specifying it in the project. * Use outdated or unsupported Node.js versions. **Why:** Ensuring consistent Node.js versions across development, staging, and production environments prevents unexpected compatibility issues and utilizes the latest features and security patches. **Example ".nvmrc":** """ 20.x """ ### 1.2 Editor Configuration **Standard:** Configure your editor for consistent formatting, linting, and auto-completion. **Do This:** * Use editor configurations like ".editorconfig", ESLint, and Prettier. * Install relevant editor extensions (e.g., ESLint, Prettier, TypeScript) for real-time feedback. **Don't Do This:** * Rely on default editor settings without customization. * Ignore linting and formatting errors in your editor. **Why:** Consistent code formatting and immediate feedback on errors improve readability and reduce the likelihood of syntax and style issues. **Example ".editorconfig":** """editorconfig root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.js, *.jsx, *.ts, *.tsx] indent_style = space indent_size = 2 [*.md] trim_trailing_whitespace = false """ ### 1.3 ESLint Configuration **Standard:** Use ESLint with recommended Next.js and React rules to enforce coding standards. **Do This:** * Install ESLint and relevant plugins: "@next/eslint-plugin-next", "eslint-plugin-react", "eslint-plugin-react-hooks". * Extend recommended configurations in your ".eslintrc.js" file. * Configure ESLint to automatically fix fixable errors. **Don't Do This:** * Disable important ESLint rules without a valid reason. * Ignore ESLint warnings and errors. **Why:** ESLint helps catch potential errors, enforces code style, and promotes best practices. **Example ".eslintrc.js":** """javascript module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:@next/next/recommended', ], parser: '@typescript-eslint/parser', plugins: ['react', 'react-hooks', '@typescript-eslint'], rules: { 'no-unused-vars': 'warn', 'react/prop-types': 'off', 'react/react-in-jsx-scope': 'off', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', '@next/next/no-img-element': 'off' // Allow standard <img> for non-optimized use cases }, settings: { react: { version: 'detect', }, }, env: { browser: true, node: true, es6: true, }, }; """ ### 1.4 Prettier Configuration **Standard:** Use Prettier to automatically format code and enforce consistent styling. **Do This:** * Install Prettier and the ESLint integration plugin "eslint-config-prettier". * Create a ".prettierrc.js" file to configure Prettier options. * Add a Prettier script to your "package.json" and run it before committing. **Don't Do This:** * Use inconsistent code formatting in your project. * Ignore Prettier warnings and errors. **Why:** Prettier ensures codebase-wide consistent code formatting, improving readability and reducing merge conflicts. **Example ".prettierrc.js":** """javascript module.exports = { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 120, tabWidth: 2, }; """ ### 1.5 TypeScript Configuration **Standard:** Use TypeScript for static typing, with strict compiler options. **Do This:** * Use TypeScript's strict mode by enabling "strict: true" in your "tsconfig.json". * Address all TypeScript errors and warnings. **Don't Do This:** * Disable strict mode or ignore TypeScript errors without justification. * Use excessive "any" types. **Why:** TypeScript enhances code quality, maintainability, and prevents runtime errors by providing static type checking. **Example "tsconfig.json":** """json { "compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } """ ## 2. Package Management ### 2.1 Dependency Management **Standard:** Use "npm" or "yarn" or "pnpm" for managing dependencies. Use "pnpm" for monorepo as well. **Do This:** * Prefer declarative dependency versions in "package.json". * Use "pnpm" for monorepo package management. Use workspace concept for development. **Don't Do This:** * Install global packages unless strictly necessary. * Install unnecessary dependencies. **Why:** Consistent dependency management ensures reproducible builds and simplifies collaboration. **Example "package.json":** """json { "name": "my-nextjs-app", "version": "1.0.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "format": "prettier --write .", "type-check": "tsc --noEmit" }, "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.5", "axios": "^1.4.0", "next": "latest", "react": "18.2.0", "react-dom": "18.2.0", "swr": "^2.2.0" }, "devDependencies": { "@types/node": "20.3.1", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", "eslint": "8.43.0", "eslint-config-next": "13.4.6", "eslint-config-prettier": "^8.8.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^2.8.8", "typescript": "5.1.3" } } """ ### 2.2 Lockfiles **Standard:** Always commit the lockfile ("package-lock.json", "yarn.lock", or "pnpm-lock.yaml") to ensure consistent dependency versions across environments. **Do This:** * Commit the generated lockfile to the repository. * Use "ci" command of the chosen package manager in ci to detect inconsistency. **Don't Do This:** * Ignore or delete the lockfile. **Why:** Lockfiles guarantee that all team members and deployment environments use exactly the same dependency versions, eliminating potential version-related issues. ## 3. Recommended Libraries and Tools ### 3.1 State Management **Standard:** Use SWR or React Query for data fetching and caching. Use Zustand or Jotai for client-side state management. **Do This:** * Use SWR for data fetching to leverage its built-in caching and revalidation features. * Use Zustand for simple global state. * Use Jotai for atom based state management. * Consider Redux or Zustand for complex application state requirements. **Don't Do This:** * Rely on global variables or context for component-specific state. * Overuse Redux for simple state needs. **Why:** These libraries provide efficient data fetching, caching, and state management, improving performance and developer experience. **Example using SWR:** """javascript import useSWR from 'swr'; const fetcher = (url: string) => fetch(url).then(res => res.json()); function Profile() { const { data, error } = useSWR('/api/user', fetcher); if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data.name}!</div> } """ **Example using Zustand:** """javascript import create from 'zustand'; interface BearState { bears: number; increasePopulation: () => void; removeAllBears: () => void; } const useStore = create<BearState>((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })) function BearCounter() { const bears = useStore((state) => state.bears) return <h1>{bears} around here ...</h1> } function BearButton() { const increasePopulation = useStore((state) => state.increasePopulation) return <button onClick={increasePopulation}>one up</button> } """ ### 3.2 Styling **Standard:** Use Styled Components, Emotion, or Material UI for styling. **Do This:** * Use a consistent styling approach throughout the project. * Leverage theme providers for centralized theme definition. * Use CSS modules for component level styling. **Don't Do This:** * Use inline styles excessively. * Mix multiple styling approaches in the same project. **Why:** These libraries offer component-level scoping, dynamic styling, and maintainability improvements. **Example using Styled Components:** """javascript import styled from 'styled-components'; const Button = styled.button" background-color: palevioletred; color: white; font-size: 16px; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; &:hover { background-color: hotpink; } "; function MyComponent() { return <Button>Click me</Button>; } """ **Example using Material UI:** """jsx import Button from '@mui/material/Button'; function MyComponent() { return ( <Button variant="contained" color="primary"> Hello World </Button> ); } """ ### 3.3 Form Handling **Standard:** Use React Hook Form or Formik for handling forms. **Do This:** * Choose a form library and stick to it consistently. * Use form validation to prevent invalid data submission. **Don't Do This:** * Manually manage form state with "useState" for complex forms. **Why:** These libraries simplifies form handling logic, validation, and submission. **Example using React Hook Form:** """javascript import { useForm } from 'react-hook-form'; function MyForm() { const { register, handleSubmit, formState: { errors } } = useForm(); const onSubmit = data => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> <input type="text" {...register("firstName", { required: true })} /> {errors.firstName && <span>This field is required</span>} <input type="submit" /> </form> ); } """ ### 3.4 Date Handling **Standard:** Use "date-fns" or "dayjs" for date and time manipulation. **Do This:** * Choose a date library and stick to it consistently. * Format dates using a consistent format. **Don't Do This:** * Rely on the native "Date" object for complex date operations. **Why:** These libraries provide powerful and consistent date manipulation functionalities. **Example using date-fns:** """javascript import { format } from 'date-fns' const formattedDate = format(new Date(), 'yyyy-MM-dd'); console.log(formattedDate); // Outputs: 2024-01-01 (or current date) """ ### 3.5 HTTP Requests **Standard:** Use "axios" or the built-in "fetch" API for making HTTP requests. **Do This:** * Create a dedicated API client for handling HTTP requests. * Handle errors and edge cases gracefully. **Don't Do This:** * Make raw HTTP requests directly in components. **Why:** "axios" and "fetch" provides a cleaner way to manage HTTP requests, handle errors, and configure request options. **Example using Axios:** """javascript import axios from 'axios'; const apiClient = axios.create({ baseURL: '/api', timeout: 5000, headers: { 'Content-Type': 'application/json', }, }); apiClient.get('/users') .then(response => { console.log(response.data); }) .catch(error => { console.error(error); }); """ ### 3.6 Internationalization **Standard:** Use "next-i18next" (or "next-intl") for easy i18n routing, language detection, SEO and locale loading. **Do This:** * Use "next-i18next" to handle internationalization concerns. * Organize translations in separate files for each locale. **Don't Do This:** * Hardcode text in components. * Implement i18n manually. **Why:** "next-i18next" simplifies the process of internationalizing Next.js applications, improving SEO and user experience. **Example "next-i18next.config.js":** """javascript module.exports = { i18n: { defaultLocale: 'en', locales: ['en', 'fr', 'de'], }, }; """ """javascript // Example usage in a component import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; export default function MyComponent() { const { t } = useTranslation('common'); return <h1>{t('welcome')}</h1>; } export const getServerSideProps = async ({ locale }) => ({ props: { ...(await serverSideTranslations(locale, ['common'])), }, }); """ ### 3.7 Testing Libraries **Standard:** Use Jest and React Testing Library for unit and integration testing. Use Cypress or Playwright for end-to-end testing. **Do This:** * Write unit tests for individual components and functions. * Write integration tests to ensure components work together correctly. * Write end-to-end tests to simulate user interactions. **Don't Do This:** * Skip testing entirely. * Write only superficial tests. **Why:** Testing ensures code reliability and prevents regressions during development. **Example Jest test:** """javascript import { render, screen } from '@testing-library/react'; import MyComponent from './MyComponent'; test('renders learn react link', () => { render(<MyComponent />); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); }); """ ## 4. Next.js Specific Tooling ### 4.1 "next/image" Optimization **Standard:** Always use the "next/image" component for image optimization. **Do This:** * Use "next/image" for all images displayed in your application. * Configure "loader" and "domains" for external images in "next.config.js". **Don't Do This:** * Use standard "<img>" tags without optimization. **Why:** "next/image" provides automatic image optimization, lazy loading, and responsive resizing. **Example using "next/image":** """javascript import Image from 'next/image'; function MyImage() { return ( <Image src="/images/profile.jpg" alt="My Profile" width={500} height={500} /> ); } """ ### 4.2 "next/link" for Navigation **Standard:** Use the "next/link" component for internal navigation to enable client-side routing. **Do This:** * Wrap "<a>" tags inside "next/link" for navigation. * Use "prefetch" prop on "next/link" for faster navigation. **Don't Do This:** * Use standard "<a>" tags for internal navigation. **Why:** "next/link" provides client-side routing, improving user experience and performance. **Example using "next/link":** """javascript import Link from 'next/link'; function MyNavigation() { return ( <Link href="/about" prefetch={true}> <a>About Us</a> </Link> ); } """ ### 4.3 "next/script" for Third-Party Scripts **Standard:** Use "next/script" component for loading third party scripts **Do This:** * Load third party scripts like google analytics, etc. using this component. * Use "strategy" attribute (beforeInteractive, afterInteractive, lazyOnload) to optimize loading strategy. **Don't Do This:** * Load scripts inside "useEffect" without a strategy. * Load scripts in a traditional manner. **Why:** "next/script" provides optimized loading of scripts and avoid blocking of main thread. **Example using "next/script":** """javascript import Script from 'next/script' export default function MyApp({ Component, pageProps }) { return ( <> <Component {...pageProps} /> <Script src="https://www.googletagmanager.com/gtag/js?id=<GA_MEASUREMENT_ID>" strategy="afterInteractive" /> <Script id="google-analytics" strategy="afterInteractive"> {" window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '<GA_MEASUREMENT_ID>'); "} </Script> </> ) } """ ### 4.4 API Routes **Standard:** Use Next.js API routes ("/pages/api") for serverless functions. **Do This:** * Implement API logic in API routes. * Handle different HTTP methods (GET, POST, PUT, DELETE) appropriately.. **Don't Do This:** * Implement complex business logic in client-side components. **Why:** API routes provide a convenient way to create serverless functions in Next.js, simplifying backend development. **Example API Route:** """javascript // /pages/api/users.js export default async function handler(req, res) { if (req.method === 'GET') { // Fetch users from database res.status(200).json([{ id: 1, name: 'John Doe' }]); } else if (req.method === 'POST') { // Create a new user const { name } = req.body; res.status(201).json({ id: 2, name }); } else { res.status(405).json({ message: 'Method Not Allowed' }); } } """ ### 4.5 Environment Variables **Standard:** Use environment variables for configuration. Access environment variables using "process.env". Securely manage secrets using ".env.local" and Vercel's environment variable management. **Do This:** * Prefix all client-side env variables with 'NEXT_PUBLIC_' **Why:** Protects sensitive information and allows different configurations for dev, staging, and production. **Example:** """javascript const apiKey = process.env.NEXT_PUBLIC_API_KEY; // use process.env to retrieve API Key """ ## 5. Continuous Integration/Continuous Deployment (CI/CD) ### 5.1 CI/CD Pipelines **Standard**: Use a CI/CD pipeline for automated testing, linting, and deployment. **Do This:** * Set up a CI/CD pipeline using tools like GitHub Actions, GitLab CI, or Jenkins. * Automate linting, testing, and building processes. * Deploy to platforms like Vercel, Netlify, or AWS. **Don't Do This:** * Manually deploy changes to production. * Skip automated testing in the CI/CD pipeline. **Example GitHub Actions workflow:** """yaml # .github/workflows/deploy.yml name: Deploy to Vercel on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: '16' - run: npm install - run: npm run lint - run: npm run test - name: Deploy to Vercel run: vercel deploy --prebuilt --token ${secrets.VERCEL_TOKEN} """ ### 5.2 Monitoring and Logging **Standard**: Implement monitoring and logging to track application performance and errors. **Do This**: * Use tools like Sentry, Datadog, or New Relic for error tracking and performance monitoring. * Implement centralized logging using tools like Winston or Bunyan. **Why**: Enables quick identification and resolution of issues in production. ## 6. Performance Optimization ### Code Splitting and Lazy Loading **Standard**: Use dynamic "import()" for code splitting and lazy-loading components. **Do This**: * Split code into smaller chunks to reduce initial load time. * Lazy-load components that are not immediately needed. **Why**: Improves initial load time and overall performance. **Example**: """javascript import React, { Suspense } from 'react'; const MyComponent = React.lazy(() => import('./MyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense> ); } """ ## 7. Monorepos For larger projects consider using a Monorepo Architecture **Standard**: Employ a monorepo structure using tools like "pnpm workspaces" or "Turborepo" for multi-package projects. **Do This**: * Use shared components and utilities across different parts of the application. * Abstract features as packages for better code reusability. **Why**: Facilitates code reuse, simplifies dependency management, and streamlines development workflows. **Example**: """json // pnpm-workspace.yaml packages: - 'apps/*' - 'packages/*' """ ## 8. Security ### 8.1 Dependencies Security **Standard:** Utilize tools like "npm audit" or "yarn audit" to identify and fix security vulnerabilities in dependencies. **Do This:** * Regularly audit dependencies for vulnerabilities. * Update dependencies to patched versions. **Why:** Keeps the application secure by addressing dependency vulnerabilities. ### 8.2 Secrets Management **Standard:** Never commit sensitive information like API keys, database passwords, or private keys directly to the repository. **Do This:** * Store secrets in secure environment variables or a secrets management system. * Use tools like "dotenv" for development and platform-specific mechanisms for production (e.g., Vercel's environment variables, AWS Secrets Manager). **Why:** Prevents unauthorized access to sensitive data. ### 8.3 Input Validation **Standard:** Validate and sanitize all user inputs to prevent injection attacks and data integrity issues. **Do This:** * Implement validation on both client and server sides. * Use libraries like "validator.js" or "joi" for input validation. **Why:** Protects against security vulnerabilities and ensures data integrity. Adherence to these standards will result in a more maintainable, performant, scalable, and secure Next.js application. This document provides a solid foundation for developers and AI coding assistants.
# Core Architecture Standards for Next.js This document outlines the core architectural standards for Next.js projects. Following these guidelines will promote maintainability, scalability, performance, and security within your applications. These standards are based on the latest versions of Next.js and related technologies. ## 1. Fundamental Architectural Patterns Choosing the correct architectural patterns is crucial for long-term project success. These standards advocate for a modular and scalable approach. ### 1.1 Modular Architecture **Standard:** Structure your Next.js application into independent, reusable modules or components. This aligns with Next.js's component-based nature and promotes code reuse, testability, and maintainability. * **Do This:** Organize code into logical modules, each with a specific responsibility. Use "src/" directory at the project root to group features into directories. * **Don't Do This:** Create large, monolithic components that handle multiple unrelated tasks. Avoid tight coupling between different parts of the application. * **Why:** Modularity reduces complexity, making code easier to understand, debug, and modify. It also promotes collaboration among developers. **Example:** """javascript // src/components/ProductCard/ProductCard.jsx import Image from 'next/image'; import Link from 'next/link'; function ProductCard({ product }) { return ( <Link href={"/products/${product.id}"} key={product.id} className="card"> <Image src={product.image} alt={product.name} width={300} height={200} className="card-image"/> <div className="card-body"> <h2>{product.name}</h2> <p>{product.description}</p> <p>Price: ${product.price}</p> </div> </Link> ); } export default ProductCard; """ """javascript // src/pages/index.jsx import ProductCard from '../components/ProductCard/ProductCard'; async function getProducts() { const res = await fetch('https://fakestoreapi.com/products'); // Replace with your API const products = await res.json(); return products; } export default async function Home() { const products = await getProducts(); return ( <div className="container"> <h1>Welcome to the Store</h1> <div className="grid"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> </div> ); } """ ### 1.2 Layered Architecture **Standard:** Separate your application into distinct layers: Presentation (UI), Application (Business Logic/Orchestration), and Data Access. Follow the principles of Separation of Concerns. * **Do This:** Create separate directories for components (Presentation), services (Application), and data access logic. Use interfaces to define contracts between layers. Isolate API calls within the Data Access layer. * **Don't Do This:** Directly embed data fetching or business logic within UI components. Mix UI concerns with API calls. * **Why:** Layered architecture improves code organization, testability, and maintainability. It allows you to change one layer without affecting others, providing flexibility for future modifications. **Example:** """javascript // src/services/productService.js (Application Layer) import { getProducts } from '../data/productRepository'; // Data Access Layer export async function fetchProducts() { // Business logic, e.g., filtering, sorting, applying discounts const products = await getProducts(); return products.map(product => ({ ...product, discountedPrice: product.price * 0.9, // Apply a 10% discount })); } """ """javascript // src/data/productRepository.js (Data Access Layer) export async function getProducts() { const res = await fetch('https://fakestoreapi.com/products'); // Replace with your API const products = await res.json(); return products; } """ """javascript // src/pages/index.jsx (Presentation Layer) import { fetchProducts } from '../services/productService'; import ProductCard from '../components/ProductCard/ProductCard'; export default async function Home() { const products = await fetchProducts(); return ( <div className="container"> <h1>Welcome to the Store</h1> <div className="grid"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> </div> ); } """ ### 1.3 Domain-Driven Design (DDD) **Standard:** Align your architecture with your business domain. Model your code based on domain concepts, entities, and value objects. * **Do This:** Identify core domain concepts and create corresponding classes or data structures. Use a ubiquitous language (shared terminology) between developers and domain experts. * **Don't Do This:** Design your application around technical concerns rather than business requirements. Use generic names that don't reflect the domain. * **Why:** DDD results in a more understandable and maintainable codebase that accurately reflects the business. It simplifies communication and ensures the software solves the right problem. **Example:** Imagine an e-commerce application. Instead of just having a generic "Product" object, you might define more specific domain entities, such as "PhysicalProduct" and "DigitalProduct", each with its own properties and behaviors: """javascript // src/domain/PhysicalProduct.js class PhysicalProduct { constructor(id, name, price, weight) { this.id = id; this.name = name; this.price = price; this.weight = weight; } calculateShippingCost(shippingRate) { return this.weight * shippingRate; } } export default PhysicalProduct; """ """javascript // src/domain/DigitalProduct.js class DigitalProduct { constructor(id, name, price, downloadLink) { this.id = id; this.name = name; this.price = price; this.downloadLink = downloadLink; } getDownloadLink() { return this.downloadLink; } } export default DigitalProduct; """ ## 2. Project Structure and Organization A well-defined project structure is essential for navigating and maintaining a Next.js application. ### 2.1 Standard Directory Structure **Standard:** Use a consistent and predictable directory structure. This promotes discoverability and reduces the cognitive load for developers. * **Do This:** Adhere to the following basic structure: """ my-nextjs-app/ ├── src/ │ ├── pages/ # Next.js Pages Router │ ├── app/ # Next.js App Router (if using) │ ├── components/ # Reusable UI components │ ├── services/ # Business logic services │ ├── data/ # Data access logic │ ├── models/ # Data models / Typescript interfaces │ ├── utils/ # Utility functions │ ├── styles/ # Global styles and component-specific styles │ ├── context/ # React Context providers │ └── lib/ # Helper functions and libraries ├── public/ # Static assets (images, fonts, etc.) ├── next.config.js # Next.js configuration ├── package.json # Project dependencies and scripts └── README.md """ * **Don't Do This:** Randomly place files and folders without a clear organizational principle. Mix different types of code within the same directory. * **Why:** A consistent structure makes it easy to find specific files and understand the application's overall organization. New developers can quickly grasp the project's layout. ### 2.2 Component Organization **Standard:** Organize UI components within the "components/" directory based on their function or domain. * **Do This:** Group related components into subdirectories. For example: """ src/components/ ├── ProductList/ │ ├── ProductList.jsx │ └── ProductItem.jsx ├── ProductCard/ │ └── ProductCard.jsx ├── Layout/ │ ├── Layout.jsx │ ├── Navbar.jsx │ └── Footer.jsx └── UI/ # Generic UI elements ├── Button.jsx └── Input.jsx """ * **Don't Do This:** Keep all components in a single flat list within the "components/" directory. Create deeply nested directory structures that are difficult to navigate. * **Why:** Logical component organization makes it easier to find, reuse, and maintain UI elements. Improves component discoverability. ### 2.3 Utilizing the "app" Router (Next.js 13+) **Standard**: When using Next.js 13 (or later) and embracing the *app* router, structure the "app" directory according to route segments, layout conventions, and server/client component distinctions. * **Do This**: * Use folders to define routes ("app/products/[id]/page.jsx" represents the "/products/:id" route). * Leverage "layout.jsx" files for shared UI across segments. * Clearly mark server components (default) and client components ("'use client'"). * Use "loading.jsx" for suspense boundaries and loading states. * Use "error.jsx" for error handling. * Utilize "route.jsx" or "route.ts" for defining API routes. * Consider using "middleware.ts" or "middleware.jsx" for request pre-processing. * **Don't Do This**: * Mix server and client component logic without clear separation. * Overcomplicate layouts with excessive nesting. * Neglect error handling or loading states. * Rely excessively on client-side data fetching when server components are more appropriate. * **Why**: The "app" router encourages a more declarative, server-centric approach to building Next.js applications. It streamlines data fetching, improves performance, and simplifies routing. Clear separation of server and client components is essential for predictable behavior. **Example:** """ app/ ├── products/ │ ├── [id]/ │ │ ├── page.jsx // Product detail page (Server Component) │ │ ├── loading.jsx // Loading state for the product detail │ │ └── layout.jsx // Layout specific to product pages │ ├── page.jsx // Product list page (Server Component) ├── api/ │ ├── products/ │ │ └── route.js // API route to fetch products ├── layout.jsx // Root layout for the entire application ├── page.jsx // Home page (Server Component) """ """javascript // app/products/[id]/page.jsx (Server Component) import { getProduct } from '@/lib/data'; // Data fetching utility export default async function ProductPage({ params }) { const product = await getProduct(params.id); if (!product) { return <div>Product not found</div>; } return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <p>Price: ${product.price}</p> </div> ); } """ ## 3. Data Fetching Strategies Choosing the right data fetching strategy can significantly impact performance and SEO. The method you use depends on whether you are using the pages directory or the app directory router. ### 3.1 Server-Side Rendering (SSR) **Standard:** Use SSR for pages that require up-to-date data or are important for SEO. * **Do This:** In the "pages" directory, use "getServerSideProps" to fetch data on each request. """javascript // pages/products/[id].jsx export async function getServerSideProps(context) { const { id } = context.params; const res = await fetch("https://fakestoreapi.com/products/${id}"); // Replace with your API const product = await res.json(); return { props: { product, }, }; } function ProductPage({ product }) { return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); } export default ProductPage; """ In the "app" directory, just fetch the data directly in the server component. """javascript // app/products/[id]/page.jsx (Server Component) import { getProduct } from '@/lib/data'; // Data fetching utility export default async function ProductPage({ params }) { const product = await getProduct(params.id); if (!product) { return <div>Product not found</div>; } return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); } """ * **Don't Do This:** Use client-side data fetching for content that should be indexed by search engines. Overuse SSR for static content. * **Why:** SSR ensures that search engines can crawl and index the content, improving SEO. It also guarantees that users always see the latest data. ### 3.2 Static Site Generation (SSG) **Standard:** Use SSG for pages with content that doesn't change frequently. * **Do This:** In the "pages" directory, use "getStaticProps" to fetch data at build time. Use "getStaticPaths" to pre-render dynamic routes. """javascript // pages/products/[id].jsx export async function getStaticPaths() { const res = await fetch('https://fakestoreapi.com/products'); // Replace with your API const products = await res.json(); const paths = products.map(product => ({ params: { id: product.id.toString() }, })); return { paths, fallback: false, // or 'blocking' }; } export async function getStaticProps(context) { const { id } = context.params; const res = await fetch("https://fakestoreapi.com/products/${id}"); // Replace with your API const product = await res.json(); return { props: { product, }, }; } function ProductPage({ product }) { return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); } export default ProductPage; """ In the "app" directory, static generation is the default for server components, so you don't need special methods. * **Don't Do This:** Use SSG for pages with frequently changing data. Forget to define "getStaticPaths" for dynamic routes. * **Why:** SSG provides excellent performance because the pages are pre-rendered at build time and served from a CDN. It reduces server load and improves Time to First Byte (TTFB). ### 3.3 Client-Side Data Fetching (CSR) **Standard:** When using the "pages" router, use CSR for interactive features or content that is personalized to the user. However, minimize CSR and favor SSR/SSG in the "app" directory. * **Do This:** Use libraries like "swr" or "react-query" to manage client-side data fetching and caching in the pages router. In the "app" router, use server components as frequently as possible. """javascript // pages/profile.jsx import useSWR from 'swr'; const fetcher = (url) => fetch(url).then((res) => res.json()); function Profile() { const { data, error } = useSWR('/api/user', fetcher); if (error) return <div>Failed to load user</div>; if (!data) return <div>Loading...</div>; return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ); } export default Profile; """ When using the app router, use "use client" to mark a client component and then use the "useEffect" hook or a library like "swr" or "react-query" inside of this component. """javascript // app/components/profile.jsx 'use client'; import useSWR from 'swr'; import { useEffect, useState } from 'react'; const fetcher = (url) => fetch(url).then((res) => res.json()); function Profile() { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); const { data, error } = useSWR('/api/user', fetcher); if (error) return <div>Failed to load user</div>; if (!data) return <div>Loading...</div>; return ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ); } export default Profile; """ * **Don't Do This:** Rely on CSR for critical content that needs to be indexed by search engines. Use CSR without proper error handling or loading indicators. * **Why:** CSR allows for dynamic and interactive user interfaces. Data can be fetched and updated in real time without requiring a full page reload. *Using the "app" directory aims to minimize CSR where it makes sense to use server components. ### 3.4 Incremental Static Regeneration (ISR) **Standard:** Use ISR for pages that need to be updated periodically without rebuilding the entire site. Only applicable to the "pages" directory. SSG with on-demand revalidation is the new favored method of regenerating data, available in the "app" directory. * **Do This:** Add a "revalidate" key to "getStaticProps" to specify how often Next.js should regenerate the page in the background. """javascript // pages/products.jsx export async function getStaticProps() { const res = await fetch('https://fakestoreapi.com/products'); // Replace with your API const products = await res.json(); return { props: { products, }, revalidate: 60, // Regenerate every 60 seconds }; } function ProductList({ products }) { return ( <ul> {products.map(product => ( <li key={product.id}>{product.name}</li> ))} </ul> ); } export default ProductList; """ * **Don't Do This:** Use ISR with excessively short revalidation intervals, as this can increase server load. * **Why:** ISR provides a balance between SSG and SSR. Pages are pre-rendered for performance, but they are also updated periodically to reflect changes in the data. This helps optimize performance and use the most up-to-date data possible. Note - this strategy is not available using the new "app" directory. * SSG with on-demand revalidation replaces this functionality for the "app" directory. ### 3.5 Server Actions ("app" router) **Standard:** Leverage Server Actions for handling form submissions and data mutations directly on the server, bypassing the need for a separate API layer in many cases. * **Do This:** Define asynchronous functions marked with "use server" inside a server component or in a dedicated file. Pass these actions directly to form elements or event handlers in client components. * **Don't Do This:** Perform complex business logic or data transformations directly within client components. Avoid unnecessary client-side state updates when the server can handle the operation. * **Why:** Server Actions simplify data handling, reduce client-side JavaScript, and can improve security by keeping sensitive logic on the server. They also integrate seamlessly with React Suspense for improved user experience. **Example:** """javascript // app/actions.js 'use server' import { revalidatePath } from 'next/cache'; import { saveProduct } from './lib/data'; export async function createProduct(formData) { const rawFormData = { name: formData.get('name'), price: parseFloat(formData.get('price')), }; await saveProduct(rawFormData); revalidatePath('/products'); // Invalidate the cache for the products page } """ """javascript // app/products/page.jsx import { createProduct } from '../actions'; export default function ProductsPage() { return ( <form action={createProduct}> <input type="text" name="name" placeholder="Product Name" /> <input type="number" name="price" placeholder="Price"/> <button type="submit">Add Product</button> </form> ); } """ ## 4. Common Anti-Patterns and Mistakes Avoiding common mistakes is equally important as following best practices. * **Over-fetching data:** Only fetch the data that is required for a particular component or page. Use GraphQL or similar technologies to precisely specify data requirements. * **Tight coupling:** Minimize dependencies between different parts of the application. Use interfaces and abstraction layers to decouple components and services. * **Ignoring performance:** Pay attention to page load times, rendering performance, and data fetching efficiency. Use tools like Lighthouse and the Next.js Profiler to identify bottlenecks and optimize performance. * **Lack of error handling:** Implement robust error handling to gracefully handle unexpected errors. Use error boundaries to prevent errors from crashing the entire application. * **Not using environment variables:** Store sensitive information, such as API keys and database credentials, using environment variables. Avoid hardcoding these values directly in the code. * **Neglecting accessibility:** Ensure your application is accessible to users with disabilities. Follow accessibility guidelines (WCAG) and use assistive technologies for testing. * **Skipping Typescript:** For complex applications, strongly consider using Typescript for static typing. This helps catch errors early and improves code maintainability. By adhering to these Core Architecture Standards, your Next.js projects will be better organized, more maintainable, and deliver a superior user experience. Regularly review and update these standards to keep pace with the evolving Next.js ecosystem.