# 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 = "" }) => (
{children}
);
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 (
setSearchTerm(e.target.value)}
/>
Search
);
};
export default SearchBar;
// Organisms : Header.jsx
import React from 'react';
import SearchBar from './SearchBar';
import Logo from './Logo';
const Header = ({ onSearch }) =>(
);
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 Loading users...;
}
return ;
};
export default UserListContainer;
// Presentational Component: UserList.jsx
import React from 'react';
const UserList = ({ users }) => (
{users.map((user) => (
{user.name}
))}
);
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 (
<p>Count: {count}</p>
setCount(count + 1)}>Increment
);
};
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 (
Data from Server:
{data.map(item => (
<p>{item.name}</p>
))}
);
}
//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 }) => (
Hello, {name}! You are {age} years old. {isAdult ? 'You are an adult.' : 'You are not an adult.'}
);
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 = ({ name, age, isAdult = false }) => (
Hello, {name}! You are {age} years old. {isAdult ? 'You are an adult.' : 'You are not an adult.'}
);
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 }) => (
{children}
);
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 }) => (
{children}
);
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 (
<p>Message: {message}</p>
);
};
export default Parent;
// Child Component: Child.jsx
import React from 'react';
const Child = ({ onClick }) => (
onClick('Goodbye')}>Click Me
);
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., "", "", "").
* 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., "", "") for interactive elements without ARIA attributes.
* Neglect keyboard navigation.
**Example:**
"""jsx
import React from 'react';
const AccessibleButton = ({ children, onClick, ariaLabel }) => (
{children}
);
export default AccessibleButton;
// Example usage
alert('Clicked!')} ariaLabel="Click to show alert">
Show Alert
//Form example
Name:
"""
## 3. Next.js Specific Considerations
### 3.1. Using Next.js "Link" Component
**Standard:** Use the "" component for client-side navigation between pages.
* **Why:** The "" 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 "" tags.
**Do This:**
* Wrap "" tags with "" when navigating between Next.js pages.
* "legacyBehavior={true}" when passing custom components to "Link".
**Don't Do This:**
* Use regular "" 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 }) => (
{children}
);
export default NavLink;
// Using a custom component
Blog
"""
### 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 }) => (
{posts.map((post) => (
{post.title}
))}
);
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 }) => (
{profile.name}
<p>{profile.email}</p>
);
export default ProfilePage;
"""
### 3.3. Image Optimization with "next/image"
**Standard:** Use the "" component from "next/image" for optimizing images.
* **Why:** The "" 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 "" tags with "".
* 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 "" 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 = () => (
);
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 = () => (
<p>This is the home page</p>
);
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 (
<p>Name: {name}</p>
<p>Age: {age}</p>
);
};
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 "" tags (when not using "").
* 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 = () => (
Loading...}>
);
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.
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'
# API Integration Standards for Next.js This document outlines the coding standards for API integration in Next.js applications. It aims to provide a comprehensive guide for developers to ensure maintainable, performant, and secure integration with backend services and external APIs. These guidelines are intended to be used by both human developers and AI coding assistants. ## 1. Architecture and Design ### 1.1. Layered Architecture **Standard:** Implement a layered architecture that separates the presentation layer (Next.js components) from the data access layer (API integration logic). * **Do This:** Create dedicated modules or directories for API clients and data fetching logic. * **Don't Do This:** Directly call APIs within your React components. **Why:** This promotes separation of concerns, making your code more testable, maintainable, and easier to reason about. It allows you to change the API implementation without affecting the UI components. **Code Example:** """javascript // src/lib/api-client.js // Using Next.js 14 Fetch API w/ automatic deduplication and caching export const getPosts = async () => { const res = await fetch('https://your-api.com/posts', { next: { revalidate: 60 } }); // Revalidate every 60 seconds if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); }; export const createPost = async (data) => { const res = await fetch('https://your-api.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!res.ok) { throw new Error('Failed to create post'); } return res.json(); }; """ """javascript // src/app/page.js import { getPosts } from '@/lib/api-client'; async function HomePage() { const posts = await getPosts(); return ( <div> <h1>Posts</h1> <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </div> ); } export default HomePage; """ ### 1.2. API Abstraction **Standard:** Abstract API endpoints into reusable functions or classes. * **Do This:** Create dedicated API services for each external resource. * **Don't Do This:** Repeat API calls throughout the codebase. **Why:** Abstraction allows reusability, central error handling, and easy swapping of underlying API implementations without affecting the whole application. **Code Example:** """javascript // src/lib/post-service.js import { getPosts, createPost } from './api-client'; // Assuming you have createdPosts and getPosts in api-client export const fetchPosts = async () => { try { const posts = await getPosts(); return posts; } catch (error) { console.error("Error fetching posts:", error); return []; // or throw error depending on your use case } }; export const addPost = async (postData) => { try { const newPost = await createPost(postData); return newPost; } catch (error) { console.error("Error creating post:", error); return null; // or throw error } }; """ ### 1.3. Environment Variables **Standard:** Use environment variables to store API keys, secrets, and endpoint URLs. * **Do This:** Utilize ".env.local" for local development and environment-specific variables for production (managed by your hosting provider). Use "process.env" to access environment variables. Validate on application boot. * **Don't Do This:** Hardcode API keys or secrets directly in your code. **Why:** Security. Avoids exposing sensitive information. Configuration can be tailored to distinct environments without modifying code. This is extremely critical. **Code Example:** """javascript // next.config.js require('dotenv').config(); module.exports = { env: { API_BASE_URL: process.env.API_BASE_URL, API_KEY: process.env.API_KEY, }, }; """ """javascript // src/lib/api-client.js const API_BASE_URL = process.env.API_BASE_URL; const API_KEY = process.env.API_KEY; export const getPosts = async () => { const res = await fetch("${API_BASE_URL}/posts?apiKey=${API_KEY}", { next: { revalidate: 60 } }); if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); }; """ ## 2. Data Fetching ### 2.1. "fetch" API with Next.js extensions **Standard:** Use Next.js's extended "fetch" API for data fetching in Server Components. leverage caching, deduplication and revalidation features. * **Do This:** Utilize "fetch" with "next: { revalidate: <seconds> }" for server-side data caching. Use the "cache: 'no-store'" option when necessary to bypass the cache. Use "tags" for on-demand invalidation. Use the "useSearchParams" hook to access querystring parameters in server components. * **Don't Do This:** Use "useEffect" with "useState" for fetching data on the server-side. **Why:** Using Server Components reduces client-side JavaScript, improves performance and provides more flexibility to optimize data fetching strategies. "fetch" integration by Next.js provides automatic memoization and deduplication of requests, optimizing network efficiency. **Code Example:** """javascript // src/app/posts/page.js import { getPosts } from '@/lib/api-client'; export default async function PostsPage() { const posts = await getPosts(); // Uses fetch with Next.js extensions return ( <div> <h1>Posts</h1> <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); } """ ### 2.2. Data Mutations **Standard:** Implement data mutations (POST, PUT, DELETE) via Server Actions to manage state and trigger revalidations. * **Do This:** Use Server Actions to handle form submissions and API calls that modify data. * **Don't Do This:** Execute mutations on the client-side if you can execute on the server-side. **Why:** Server Actions provide a secure and efficient way to perform data mutations directly on the server, minimizing client-side JavaScript and improving performance. They also integrate well with Next.js revalidation mechanisms. **Code Example:** """javascript // src/app/actions.js 'use server' import { revalidatePath } from 'next/cache' export async function createPost(formData) { // Validate FormData const title = formData.get('title') const content = formData.get('content') if (!title || !content) { throw new Error('Title and content are required') } // Call your createPost API endpoint const res = await fetch('https://your-api.com/posts', { method: 'POST', body: JSON.stringify({ title, content }), headers: { 'Content-Type': 'application/json' } }) if (!res.ok) { throw new Error('Failed to create post') } // Revalidate the posts page revalidatePath('/posts') } """ """javascript // src/app/posts/page.js import { createPost } from '@/app/actions'; export default function CreatePostForm() { return ( <form action={createPost}> <label htmlFor="title">Title:</label> <input type="text" id="title" name="title" required /> <label htmlFor="content">Content:</label> <textarea id="content" name="content" required /> <button type="submit">Create Post</button> </form> ); } """ ### 2.3. Error Handling **Standard**: Provide clear and informative error messages to the user for API related issues. * **Do This:** Implement try-catch blocks in API client functions and Server Actions to handle potential errors. Create user-friendly error messages based on error types. Use Next.js's error boundary component for global error handling. * **Don't Do This:** Directly expose raw error messages from the API to the client. Leave unhandled exceptions in your code. **Why:** Shows transparency and avoids confusing the user. Hides sensitive information. **Code Example:** """javascript // src/lib/api-client.js export const getPosts = async () => { try { const res = await fetch('https://your-api.com/posts'); if (!res.ok) { throw new Error("Failed to fetch posts: ${res.status} ${res.statusText}"); } return res.json(); } catch (error) { console.error("Error fetching posts:", error); throw new Error("Failed to fetch posts. Please try again later."); } }; """ """javascript // src/app/posts/page.js import { getPosts } from '@/lib/api-client'; async function PostsPage() { try { const posts = await getPosts(); return ( <div> <h1>Posts</h1> <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); } catch (error) { console.error("Error in PostsPage:", error); return ( <div> <h1>Error</h1> <p>{error.message}</p> </div> ); } } export default PostsPage; """ ### 2.4. Loading States **Standard:** Show loading indicators while data is fetched from the API. * **Do This:** Use Suspense from React in Server Components to handle loading states elegantly. Or, use conditional rendering based on a loading state variable in Client Components. * **Don't Do This:** Leave the user with a blank screen while waiting for data. **Why:** Improves user experience by giving feedback. Using Suspense avoids race conditions in rendering. **Code Example (Server Component with Suspense):** """javascript // src/app/posts/page.js import { getPosts } from '@/lib/api-client'; import { Suspense } from 'react'; async function Posts() { const posts = await getPosts(); return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) } export default function PostsPage() { return ( <div> <h1>Posts</h1> <Suspense fallback={<p>Loading posts...</p>}> <Posts /> </Suspense> </div> ); } """ ## 3. API Client Implementation ### 3.1. HTTP Client Libraries **Standard:** Use built-in "fetch" or libraries like "axios" only when necessary but favor built-in "fetch" since it is available in both server and client runtime. * **Do This:** Utilize "fetch" API for standard API requests. Use "axios" if features like request cancellation or interceptors are genuinely needed. * **Don't Do This:** Add unnecessary dependencies by using "axios" for simple API requests. **Why:** Using the built-in "fetch" API minimizes the number of external dependencies, reducing bundle size and project complexity. "axios" adds significant overhead if not needed.. ### 3.2. Request Headers **Standard:** Set appropriate request headers, including "Content-Type", "Authorization", and custom headers when required. * **Do This:** Set "Content-Type: application/json" for JSON payloads. Use "Authorization: Bearer <token>" for authentication, store tokens securely using cookies or local storage (with caution against XSS vulnerability). * **Don't Do This:** Include sensitive information in request URLs if not absolutely necessary. **Code Example:** """javascript // src/lib/api-client.js const API_BASE_URL = process.env.API_BASE_URL; export const createPost = async (data, token) => { const res = await fetch("${API_BASE_URL}/posts", { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': "Bearer ${token}", //Securely sourced token }, body: JSON.stringify(data), }); if (!res.ok) { throw new Error('Failed to create post'); } return res.json(); }; """ ### 3.3. Response Handling **Standard:** Handle API responses correctly, including status codes, headers, and data parsing. * **Do This:** Check the "res.ok" property of the response to ensure the request was successful. Parse the response body using "res.json()" for JSON APIs. Implement retry policies for transient failures. Handle different HTTP status codes (2xx, 4xx, 5xx) appropriately. * **Don't Do This:** Assume all API calls are successful without checking the response status. Ignore error responses from the API. **Code Example:** """javascript // src/lib/api-client.js const API_BASE_URL = process.env.API_BASE_URL; export const getPosts = async () => { const res = await fetch("${API_BASE_URL}/posts", { next: { revalidate: 60 }, }); if (!res.ok) { console.error("API Error: ${res.status} - ${res.statusText}"); throw new Error('Failed to fetch data. Please check the API.'); } try { const data = await res.json(); return data; } catch (error) { console.error("Error parsing JSON:", error); throw new Error("Failed to parse API response."); } }; """ ## 4. Security ### 4.1. Preventing XSS **Standard**: Sanitize all user inputs before sending them to the API. * **Do This:** Use libraries like DOMPurify to sanitize HTML content before sending it to the API. * **Don't Do This:** Directly send user-provided data to the API without any sanitation. **Why:** Prevents malicious scripts from being injected into the API and subsequently executed on other users' browsers. ### 4.2. Rate Limiting **Standard**: Implement rate limiting to protect your API from abuse. * **Do This:** Utilize serverless functions ("app/api" routes in Next.js) in combination with rate limiting libraries like "iron-session" or "upstash" to protect your APIs from abuse. Implement rate limiting on specific routes that are prone to abuse. * **Don't Do This:** Expose your API without any rate-limiting measures. **Why:** Prevents denial-of-service attacks and protects your infrastructure from overload. ### 4.3. Input Validation **Standard:** Validate all input data on both the client-side and server-side before sending it to the API. * **Do This:** Use libraries like Zod, Yup, or Joi to define schemas for your API requests. Verify the data on the client before sending, and re-validate on the server for security. * **Don't Do This:** Trust client-side validation alone. **Why:** Prevents invalid data from reaching your API and causing unexpected errors. Reduces the chance of injection attacks. ## 5. Performance Optimization ### 5.1. Caching **Standard:** Implement caching for API responses to reduce latency and server load. * **Do This:** Use Next.js's built-in "fetch" caching (with "next: { revalidate: }") for server-side data fetching. Utilize a CDN for static assets and API responses. Implement client-side caching using libraries like "swr" or "react-query" for frequently accessed data. Consider using the "stale-while-revalidate" caching strategy. * **Don't Do This:** Disable caching altogether. Cache sensitive data without proper safeguards. **Why:** Significantly improves app performance and responsiveness. Reduces the load on the API server, leading to cost savings. ### 5.2. Pagination **Standard:** Implement pagination for API endpoints that return large datasets. * **Do This:** Use offset-based or cursor-based pagination techniques. Use the "Link" header in the API response to provide links to the next and previous pages. * **Don't Do This:** Return all data at once without pagination. **Why:** Prevents the transfer of unnecessarily large datasets, improving performance. ### 5.3. Compression **Standard:** Enable compression (e.g., gzip or Brotli) for API responses. * **Do This:** Configure your API server to compress responses. * **Don't Do This:** Neglect compression, resulting in larger data transfer sizes. **Why:** Reduces the size of data transferred, improving load times. ## 6. Testing ### 6.1. Unit Testing **Standard:** Write unit tests for API client functions to ensure they correctly interact with the API. * **Do This:** Use testing frameworks like Jest and testing libraries like "msw" (Mock Service Worker) to mock API responses. Test different scenarios, including success, error, and edge cases. * **Don't Do This:** Skip unit testing for API integration. Rely solely on end-to-end tests. **Why:** Provides confidence that API client functions are working as expected. Catches errors early in the development process. **Code Example:** """javascript // src/lib/api-client.test.js import { getPosts } from './api-client'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; const mockPosts = [ { id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }, ]; const server = setupServer( rest.get('https://your-api.com/posts', (req, res, ctx) => { return res(ctx.json(mockPosts)); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe('getPosts', () => { it('should return an array of posts', async () => { const posts = await getPosts(); expect(posts).toEqual(mockPosts); }); it('should handle errors when fetching posts', async () => { server.use( rest.get('https://your-api.com/posts', (req, res, ctx) => { return res(ctx.status(500)); }) ); await expect(getPosts()).rejects.toThrow('Failed to fetch data'); }); }); """ ### 6.2. End-to-End Testing **Standard:** Write end-to-end tests to verify the entire API integration flow, including frontend components and backend services. * **Do This:** Use testing frameworks like Cypress or Playwright to simulate user interactions and verify the behavior of the application. Test different scenarios, including success, error, and edge cases. * **Don't Do This:** Skip end-to-end testing. Rely solely on unit tests. **Why:** Verifies that the entire system is working correctly. Catches integration issues that might not be caught by unit tests. ## 7. Documentation ### 7.1. API Client Documentation **Standard:** Document all API client functions, including their purpose, parameters, return values, and potential errors. * **Do This:** Use JSDoc or similar documentation generators to generate API documentation. * **Don't Do This:** Skip API client documentation. **Why:** Makes it easier for other developers to understand and use the API client. ### 7.2. API Endpoint Documentation **Standard:** Document all API endpoints used in the application, including their purpose, request parameters, response format, and potential errors. * **Do This:** Use tools like Swagger / OpenAPI to define and document API endpoints. * **Don't Do This:** Skip API endpoint documentation. **Why:** Makes it easier for frontend developers to understand and integrate with the API. ## 8. Common Anti-Patterns and Mistakes * **Over-fetching:** Fetching more data than is required by the UI. Use GraphQL, or selective field retrieval on the backend to mitigate. * **Under-fetching:** Making multiple API requests to fetch data that could be retrieved in a single request. * **N+1 Problem:** A database query is executed for each item displayed in a list. * **Tight coupling between UI and API:** Making the UI directly dependent on the API's data structure. * **Ignoring error handling:** Not handling API errors gracefully. * **Hardcoding API endpoints and credentials:** Making it difficult to change the API configuration. * **Lack of caching:** Not caching API responses, leading to unnecessary API requests. * **Inadequate testing:** Not writing unit tests and end-to-end tests for API integration. By following these coding standards, developers can ensure that their Next.js applications have robust, maintainable, performant, and secure API integrations. These guidelines are designed to enhance code quality and promote collaboration within development teams.
# 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.