# API Integration Standards for Astro
This document outlines coding standards and best practices for API integration in Astro projects. Following these guidelines will ensure maintainable, performant, and secure interactions with backend services and external APIs.
## 1. Architectural Considerations
### 1.1. Server Endpoints vs. Client-Side Fetching
**Do This:**
* Use server endpoints (Astro API routes) for sensitive operations, data mutations, and tasks requiring authentication or authorization.
* Use client-side "fetch" calls only for publicly available data, progressively enhanced experiences, or scenarios where immediate feedback is crucial.
**Don't Do This:**
* Expose API keys or sensitive logic directly in client-side JavaScript.
* Overload client-side code with complex data processing or transformations that should be handled server-side.
**Why:** Server endpoints provide a secure environment for handling sensitive data and operations, while client-side fetching can improve user experience for certain types of data retrieval.
**Example (Server Endpoint):**
"""astro
// src/pages/api/submit-form.js
import { APIRoute } from 'astro';
export const post: APIRoute = async ({ request }: APIContext) => {
if (request.headers.get("Content-Type") === "application/json") {
const body = await request.json();
// Process the data securely (e.g., database interaction, email sending)
console.log("Received form data:", body);
// Construct a success response
return new Response(
JSON.stringify({
message: 'Form submission successful!',
}),
{ status: 200 }
);
}
return new Response(null, { status: 400 });
};
"""
**Example (Client-Side Fetch):**
"""astro
---
import { useEffect, useState } from 'astro/hooks';
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch('https://api.example.com/public-data');
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
const json = await response.json();
setData(json);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
---
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
"""
### 1.2. Data Transformation and Abstraction
**Do This:**
* Create abstraction layers (e.g., utility functions, services) to encapsulate API interaction logic.
* Transform API responses into a format suitable for your Astro components, separating presentation from data source.
**Don't Do This:**
* Directly use API responses within components without transformation or validation.
* Duplicate API interaction logic throughout your application.
**Why:** Abstraction improves code reusability, testability, and maintainability by decoupling components from specific API implementations. Transformation ensures data is consistent across all components that need to use it.
**Example (Abstraction Layer):**
"""javascript
// src/lib/api-client.js
const API_BASE_URL = 'https://api.example.com';
export async function getPosts() {
const response = await fetch("${API_BASE_URL}/posts");
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
const data = await response.json();
return data.map(post => ({
id: post.id,
title: post.title,
excerpt: post.body.substring(0, 200) + '...', // abstracting to a summary
}));
}
export async function getPost(id) {
const response = await fetch("${API_BASE_URL}/posts/${id}");
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
return await response.json();
}
"""
"""astro
---
// src/pages/index.astro
import { getPosts } from '../lib/api-client';
const posts = await getPosts();
---
{posts.map(post => (
{post.title}
<p>{post.excerpt}</p>
))}
"""
## 2. Implementation Standards
### 2.1. Error Handling
**Do This:**
* Implement robust error handling for all API requests, including network errors, HTTP status codes, and data validation failures.
* Provide meaningful error messages to users and log errors for debugging purposes.
* Use "try...catch" blocks and "finally" blocks appropriately to handle asynchronous operations.
**Don't Do This:**
* Ignore potential errors or rely on default browser error messages.
* Expose sensitive error information to users.
**Why:** Proper error handling provides a better user experience, simplifies debugging, and prevents application crashes.
**Example:**
"""javascript
// src/lib/api-client.js
export async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
// Log detailed error information for debugging
console.error("API Error: ${response.status} - ${response.statusText} for URL: ${url}");
throw new Error("API request failed with status ${response.status}");
}
return await response.json();
} catch (error) {
// Log the error for server-side monitoring
console.error("An error occurred while fetching data:", error);
// Re-throw the error for the calling function to handle
throw error;
}
}
"""
"""astro
---
// src/components/MyComponent.astro
import { fetchData } from '../lib/api-client';
import { useState, useEffect } from 'astro/hooks';
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadData() {
try {
const result = await fetchData('https://api.example.com/data');
setData(result);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
loadData();
}, []);
---
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
"""
### 2.2. Data Validation
**Do This:**
* Validate both request and response data to ensure data integrity.
* Use schema validation libraries (e.g., Zod, Yup) to define and enforce data structures.
* Handle validation errors gracefully, providing informative messages to users or logging errors for debugging.
**Don't Do This:**
* Assume API data is always correct and valid.
* Perform validation directly in components, mixing validation logic with presentation logic.
**Why:** Data validation prevents unexpected errors, ensures data consistency, safeguards your application from malicious input and greatly helps with debugging.
**Example (Zod Validation):**
"""javascript
// src/lib/schemas.js
import { z } from 'zod';
export const PostSchema = z.object({
id: z.number(),
title: z.string().min(5).max(100),
body: z.string().min(10),
userId: z.number(),
});
export const CommentSchema = z.object({
id: z.number(),
postId: z.number(),
name: z.string(),
email: z.string().email(),
body: z.string()
})
export type Post = z.infer;
export type Comment = z.infer
"""
"""javascript
// src/lib/api-client.js
import { PostSchema } from './schemas';
export async function getPost(id) {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/${id}");
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
const data = await response.json();
// Validate the API response against the schema
const validatedData = PostSchema.parse(data);
return validatedData;
} catch (error) {
console.error("Error fetching and validating post:", error);
throw error;
}
}
"""
### 2.3. Authentication and Authorization
**Do This:**
* Use secure authentication and authorization mechanisms (e.g., OAuth 2.0, JWT) to protect sensitive API endpoints.
* Store authentication tokens securely (e.g., using HTTP-only cookies, server-side sessions).
* Implement role-based access control (RBAC) to restrict access to resources based on user roles.
**Don't Do This:**
* Store passwords or API keys directly in client-side JavaScript.
* Implement custom authentication schemes without proper security expertise.
* Expose sensitive information in URLs or request bodies.
**Why:** Authentication and authorization ensure that only authorized users can access protected resources, preventing data breaches and unauthorized actions.
**Example (Server Endpoint with Authentication):**
"""javascript
// src/pages/api/protected.js
import { APIRoute } from 'astro';
import { verifyJwt } from '../../lib/auth'; // Hypothetical JWT verification function
export const get: APIRoute = async ({ request }: APIContext) => {
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 401 });
}
const token = authHeader.substring(7); // Remove "Bearer "
try {
const user = await verifyJwt(token); // Verify JWT
// Access user information from the verified token
console.log("Authenticated user:", user);
// Your protected logic here
return new Response(JSON.stringify({ message: 'Protected data' }), { status: 200 });
} catch (error) {
console.error("JWT verification failed:", error);
return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 401 });
}
};
"""
### 2.4. Rate Limiting and Caching
**Do This:**
* Implement rate limiting to prevent abuse and protect your API from overload.
* Use caching strategies (e.g., HTTP caching, server-side caching) to reduce API request frequency and improve performance.
**Don't Do This:**
* Expose your API without any rate limiting measures.
* Cache sensitive data without proper security considerations.
**Why:** Rate limiting and caching improve API availability, performance, and security by mitigating abuse, reducing server load, and improving response times.
**Example (Basic Server-Side Caching):**
"""javascript
// src/pages/api/data.js
import { APIRoute } from 'astro';
const cache = new Map();
const CACHE_TTL = 60; // Cache duration in seconds
export const get: APIRoute = async () => {
const now = Math.floor(Date.now() / 1000); // Current time in seconds
if (cache.has('data') && cache.get('data').expiry > now) {
// Return cached data
console.log("Serving data from cache")
return new Response(JSON.stringify(cache.get('data').value), { status: 200 });
}
try {
// Simulate fetching data from an external API
console.log("Fetching from external API")
const response = await fetch('https://api.example.com/expensive-data');
if (!response.ok) {
throw new Error("API request failed with status ${response.status}");
}
const data = await response.json();
// Store data in the cache with an expiry timestamp
cache.set('data', {
value: data,
expiry: now + CACHE_TTL,
});
return new Response(JSON.stringify(data), { status: 200 });
} catch (error) {
console.error("Error fetching data:", error);
return new Response(JSON.stringify({ message: 'Failed to fetch data' }), { status: 500 });
}
};
"""
## 3. Performance Optimization
### 3.1. Minimize Request Size and Latency
**Do This:**
* Use efficient data formats (e.g., JSON, Protobuf) and compression techniques (e.g., gzip, Brotli).
* Request only the data you need, using query parameters or specialized API endpoints.
* Optimize images and other assets to reduce file sizes.
* Use a CDN (Content Delivery Network) to serve static assets from geographically distributed servers.
**Don't Do This:**
* Request excessive data that is not used by your application.
* Use large, unoptimized images or other assets.
**Why:** Reducing request size and latency improves page load times, reduces bandwidth consumption, and provides a better user experience.
### 3.2. Parallelize API Requests
**Do This:**
* Use "Promise.all" or similar techniques to make concurrent API requests when possible.
* Avoid making sequential API requests that block other operations.
**Don't Do This:**
* Overload the server by making too many concurrent requests. Consider the API provider rate limits and use strategies to avoid going over API quotas.
**Why:** Parallelizing API requests can significantly reduce overall loading time, especially when fetching data from multiple sources.
**Example:**
"""astro
---
import { fetchData } from '../lib/api-client';
import { useState, useEffect } from 'astro/hooks';
const [posts, setPosts] = useState(null);
const [comments, setComments] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadData() {
try {
// Fetch posts and comments concurrently
const [postsData, commentsData] = await Promise.all([
fetchData('https://jsonplaceholder.typicode.com/posts'),
fetchData('https://jsonplaceholder.typicode.com/comments')
]);
setPosts(postsData);
setComments(commentsData);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
loadData();
}, []);
---
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
Posts
{posts && (
{posts.map(post => (
{post.title}
))}
)}
Comments
{comments && (
{comments.map(comment => (
{comment.name}
))}
)}
"""
### 3.3. Streaming
**Do This:**
* Leverage streaming API endpoints where response payloads can be transmitted as a continuous stream of data, rather than waiting for the entire response to be constructed before sending it.
* Use libraries like "node-fetch" along with the "ReadableStream" API in JavaScript to handle streaming responses efficiently.
* Display data incrementally as it arrives from the stream to improve the user experience and perceived performance.
**Don't Do This:**
* Buffer the entire stream into memory before processing or displaying the data, negating the benefits of streaming.
* Assume that all API endpoints support streaming; verify that the server supports it and returns data in a streaming format.
**Why:** Streaming reduces time-to-first-byte (TTFB) and allows users to start seeing content immediately.
## 4. Security Best Practices
### 4.1. Input Sanitization and Output Encoding
**Do This:**
* Sanitize user inputs to prevent cross-site scripting (XSS) and other injection attacks.
* Encode data properly when rendering it in HTML templates to prevent XSS vulnerabilities.
**Don't Do This:**
* Trust user inputs without sanitization or validation.
* Directly render user-provided data in HTML without encoding.
**Why:** Input sanitization and output encoding prevent malicious scripts from executing in the browser, protecting users from XSS attacks.
### 4.2. CORS Configuration
**Do This:**
* Configure Cross-Origin Resource Sharing (CORS) policies to restrict which domains can access your API.
* Use a restrictive CORS policy that only allows trusted domains to access your API.
**Don't Do This:**
* Use a wildcard CORS policy ("Access-Control-Allow-Origin: *"), which allows any domain to access your API.
**Why:** CORS policies prevent unauthorized domains from making requests to your API, protecting it from cross-origin attacks. In Astro, you can configure middleware to enforce CORS.
**Example:**
"""javascript
// src/middleware.js
import { defineMiddleware } from 'astro/middleware';
import { sequence } from 'astro/middleware';
const corsHeaders = {
"Access-Control-Allow-Origin": "https://your-trusted-domain.com", // Replace with your domain
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Credentials": "true",
};
const corsMiddleware = defineMiddleware(async (context, next) => {
if (context.request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: corsHeaders,
});
}
const response = await next();
Object.entries(corsHeaders).forEach(([header, value]) => {
response.headers.set(header, value);
});
return response;
});
const loggingMiddleware = defineMiddleware(async (context, next) => {
console.log("Request received: ${context.request.method} ${context.request.url}");
const response = await next();
console.log("Response sent: ${response.status}");
return response;
});
export const onRequest = sequence(loggingMiddleware, corsMiddleware);
"""
### 4.3. Secrets Management
**Do This:**
* Store API keys, passwords, and other secrets securely using environment variables or dedicated secret management services.
* Avoid committing secrets directly to your code repository.
**Don't Do This:**
* Store secrets directly in your code or configuration files.
* Expose secrets in client-side JavaScript.
**Why:** Secure secrets management protects sensitive information from unauthorized access and prevents data breaches. Use ".env" files (and ensure they're in ".gitignore") for local development, and platform-provided environment variables in production.
## 5. API Documentation and Versioning
### 5.1. Documentation
**Do This:**
* Provide clear and comprehensive documentation for your APIs including endpoints, data types, request/response structures, and authentication methods.
* Use documentation generators (e.g., Swagger/OpenAPI) to automatically generate documentation from your code.
**Don't Do This:**
* Deploy APIs without any documentation.
* Keep documentation outdated or incomplete.
**Why:** Well-maintained documentation allow other developers to effectively use your API.
### 5.2. Versioning
**Do This:**
* Use API versioning to ensure backwards compatibility when making changes to your APIs.
* Use semantic versioning (SemVer) to clearly communicate the nature of changes made in each version.
**Don't Do This:**
* Make breaking changes to APIs without versioning.
* Fail to communicate deprecated APIs
**Why:** API versioning protects existing integrations from breaking when APIs evolve.
By adhering to these API integration standards, you can build robust, secure, and maintainable Astro applications that effectively interact with external services.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Astro This document outlines the component design standards for Astro projects. These standards promote maintainability, reusability, performance, and a consistent development experience across teams. It leverages the latest Astro features and best practices. ## 1. Component Architecture ### 1.1 Modularity and Reusability **Standard:** Components should be designed as independent, reusable units of functionality. **Do This:** * Create components with a single, well-defined responsibility. * Favor composition over inheritance. Use Astro's component slot and props functionalities to allow customization. * Store reusable components in a dedicated directory (e.g., "src/components"). **Don't Do This:** * Create monolithic components that handle multiple unrelated tasks. * Duplicate code across components. Extract common logic into shared components or utility functions. **Why:** Modular components are easier to understand, test, and maintain. Reusable components reduce code duplication and development time. **Example:** """astro // src/components/Button.astro --- interface Props { variant?: 'primary' | 'secondary'; size?: 'small' | 'medium' | 'large'; text: string; onClick?: () => void; } const { variant = 'primary', size = 'medium', text, onClick } = Astro.props; const buttonClasses = { primary: 'bg-blue-500 hover:bg-blue-700 text-white', secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800 border border-gray-400', }; const sizeClasses = { small: 'px-2 py-1 text-sm', medium: 'px-4 py-2 text-base', large: 'px-6 py-3 text-lg', }; const selectedVariantClasses = buttonClasses[variant]; const selectedSizeClasses = sizeClasses[size]; --- <button class:list={[selectedVariantClasses, selectedSizeClasses, 'font-bold rounded']}> {text} </button> """ """astro // src/pages/index.astro --- import Button from '../components/Button.astro'; --- <Button variant="primary" size="medium" text="Click me!" onClick={() => alert('Clicked!')} /> <Button variant="secondary" size="small" text="Cancel" /> """ ### 1.2 Component Grouping **Standard:** Group related components into directories based on feature or domain. **Do This:** * Create directories for specific features or sections of the application (e.g., "src/components/blog", "src/components/ui"). * Include an "index.js" or "index.ts" file in each directory to export all components from that directory. **Don't Do This:** * Dump all components into a single "src/components" directory without any organization. * Create deeply nested directory structures that are difficult to navigate. **Why:** Logical component grouping improves code organization, maintainability, and discoverability. **Example:** """ src/components/ ├── blog/ │ ├── ArticleCard.astro │ ├── ArticleList.astro │ └── index.ts // Exports ArticleCard and ArticleList ├── ui/ │ ├── Button.astro │ ├── Input.astro │ └── index.ts // Exports Button and Input └── index.ts // Exports all components blog and ui """ """typescript // src/components/blog/index.ts export { default as ArticleCard } from './ArticleCard.astro'; export { default as ArticleList } from './ArticleList.astro'; """ """typescript // src/components/index.ts export * from './blog'; export * from './ui'; """ ### 1.3 Data Fetching Responsibilities **Standard:** Limit component data fetching responsibilities. **Do This:** * Fetch data in layouts or pages and pass the data as props to components. This decouples components from specific data sources. * Use [Astro.glob](https://docs.astro.build/en/reference/api-reference/#astroglob) or the [Content Collections API](https://docs.astro.build/en/guides/content-collections/) for data fetching within "src/pages" and "src/layouts". * When absolutely necessary to fetch data within a component, isolate the data fetching logic into a separate helper function or custom hook. **Don't Do This:** * Perform complex data transformations or business logic within components. * Fetch the same data multiple times in different components. **Why:** Decoupling data fetching improves component reusability and testability. Centralized data fetching also improves performance by minimizing redundant requests. **Example:** """astro // src/pages/blog.astro --- import ArticleCard from '../components/ArticleCard.astro'; import { getCollection } from 'astro:content'; const articles = await getCollection('blog'); --- <main> <h1>Blog</h1> <ul> {articles.map((article) => ( <li> <ArticleCard article={article} /> </li> ))} </ul> </main> """ """astro // src/components/ArticleCard.astro --- interface Props { article: { slug: string; data: { title: string; description: string; }; }; } const { article } = Astro.props; --- <a href={"/blog/${article.slug}"}> <h2>{article.data.title}</h2> <p>{article.data.description}</p> </a> """ ## 2. Component Implementation Details ### 2.1 Prop Types **Standard:** Define explicit prop types for all components using TypeScript. **Do This:** * Create an "interface" named "Props" to define the types of all props. * Enforce required props using TypeScript's type system. * Provide default prop values for optional props where appropriate. * Leverage [Astro.props](https://docs.astro.build/en/reference/api-reference/#astroprops) for type safety **Don't Do This:** * Skip prop type definitions. * Use "any" or "unknown" for prop types. * Define default prop values using JavaScript's "||" operator, which can lead to unexpected behavior with falsy values. **Why:** Explicit prop types improve code readability, prevent runtime errors, and improve the developer experience. **Example:** """astro // src/components/Alert.astro --- interface Props { type: 'success' | 'warning' | 'error' | 'info'; message: string; closable?: boolean; } const { type, message, closable = false } = Astro.props; const alertClasses = { success: 'bg-green-100 border-green-400 text-green-700', warning: 'bg-yellow-100 border-yellow-400 text-yellow-700', error: 'bg-red-100 border-red-400 text-red-700', info: 'bg-blue-100 border-blue-400 text-blue-700', }; const selectedClasses = alertClasses[type]; --- <div class:list={[selectedClasses, 'border rounded p-4 mb-4 flex items-center justify-between']}> <span>{message}</span> {closable && ( <button aria-label="Close"> <svg class="w-4 h-4 fill-current" viewBox="0 0 20 20"><path d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"/></svg> </button> )} </div> """ ### 2.2 Component Styling **Standard:** Use a consistent styling approach across all components. **Do This:** * Favor component-scoped styles using Astro's built-in CSS support. * Use a CSS preprocessor (e.g., Sass, Less) for complex styling requirements. * Use a utility-first CSS framework (e.g., Tailwind CSS) for consistent styling and rapid development. Install Tailwind using "astro add tailwind". * For dynamic styling, use the ":global()" selector sparingly and only when necessary for interacting with third-party libraries or CSS frameworks. * Consider using CSS variables for theming. **Don't Do This:** * Use inline styles directly in the HTML templates. * Use global styles that can conflict with other components. * Mix different styling approaches within the same project. **Why:** Consistent styling improves code maintainability, reduces visual inconsistencies, and streamlines the development process. **Example (Tailwind CSS):** """astro // src/components/Card.astro --- interface Props { title: string; description: string; } const { title, description } = Astro.props; --- <div class="bg-white rounded-lg shadow-md p-4"> <h2 class="text-xl font-bold mb-2">{title}</h2> <p class="text-gray-700">{description}</p> </div> """ ### 2.3 Component State **Standard:** Minimize component state and use it judiciously. **Do This:** * Prefer stateless functional components whenever possible. * When state is necessary, use Astro's client-side directives ("client:load", "client:idle", "client:visible", "client:media") combined with a front-end framework like Solid, React or Vue to manage component state. * Consider using a global state management library (e.g., Context API with React, Zustand or Jotai) for complex application state. **Don't Do This:** * Store unnecessary data in component state. * Mutate component state directly without using the appropriate setter functions or framework mechanisms. * Overuse global state management for simple component-specific state. **Why:** Minimizing component state improves performance and reduces complexity. Using appropriate state management techniques improves data consistency and prevents unexpected side effects. **Example (SolidJS with Astro):** """astro --- // src/components/Counter.astro --- <Counter client:visible /> <script> import { render } from 'solid-js/web'; import { createSignal } from 'solid-js'; function Counter() { const [count, setCount] = createSignal(0); return ( <div> <p>Count: {count()}</p> <button onClick={() => setCount(count() + 1)}>Increment</button> </div> ); } render(Counter, document.querySelector('counter')) </script> """ ### 2.4 Naming Conventions **Standard:** Follow consistent naming conventions for components and props. **Do This:** * Use PascalCase for component filenames (e.g., "MyComponent.astro"). * Use camelCase for prop names (e.g., "myProp", "onClick"). * Use descriptive and concise names that clearly indicate the component's purpose. * Use plural names for components that display a list of items (e.g., "ArticleList"). **Don't Do This:** * Use inconsistent naming conventions across the project. * Use abbreviations or acronyms that are not widely understood. * Use generic names that do not accurately reflect the component's functionality. **Why:** Consistent naming improves code readability and makes it easier to understand the purpose of components and props. ## 3. Performance Optimization ### 3.1 Component Hydration **Standard:** Use Astro's partial hydration features judiciously to optimize performance. **Do This:** * Use the "client:" directives ("client:load", "client:idle", "client:visible", "client:media") to control when components are hydrated. * Start with "client:idle" or "client:visible" for most interactive components to defer hydration until the browser is idle or the component is visible in the viewport. * Use "client:load" only for components that are critical to the initial user experience. * Consider using "<astro-island>" for even more granular control over hydration. **Don't Do This:** * Hydrate all components on page load, which can negatively impact performance. * Use "client:only" unless you are absolutely sure that the component will only ever be rendered on the client-side. **Why:** Partial hydration significantly improves page load performance by only hydrating the components that need to be interactive. **Example:** """astro // src/components/InteractiveComponent.astro --- // This component will be hydrated when it becomes visible in the viewport. --- <div client:visible> {/* Interactive content here */} </div> """ ### 3.2 Image Optimization **Standard:** Optimize images for performance using Astro's built-in image optimization features or a third-party library. **Do This:** * Use the "<Image />" component from "@astrojs/image" to automatically optimize images. * Specify the "width" and "height" props for all images to prevent layout shifts. * Use the "format" prop to convert images to modern formats like WebP or AVIF. * Use the "quality" prop to control the level of compression. * Consider using a CDN (Content Delivery Network) to serve images from a location closer to the user. **Don't Do This:** * Use large, unoptimized images directly in the HTML templates. * Skip specifying the "width" and "height" props for images. * Serve images from the same server as the application, which can negatively impact performance. **Why:** Image optimization reduces image file sizes, improves page load times, and enhances the user experience. **Example:** """astro // src/components/HeroImage.astro --- import { Image } from '@astrojs/image/components'; import heroImage from '../assets/hero.jpg'; --- <Image src={heroImage} alt="Hero Image" width={1200} height={600} format="webp" quality={80} /> """ ### 3.3 Lazy Loading **Standard:** Use lazy loading for images and other resources that are not immediately visible in the viewport. **Do This:** * Use the "loading="lazy"" attribute for images to defer loading until they are near the viewport. * Consider using a third-party library or custom implementation for lazy loading other types of resources, such as iframes or videos. **Don't Do This:** * Lazy load resources that are above the fold or critical to the initial user experience. **Why:** Lazy loading improves page load performance by deferring the loading of non-critical resources until they are needed. **Example:** """astro <img src="/path/to/image.jpg" alt="Description of image" loading="lazy"> """ ## 4. Security Considerations ### 4.1 Input Sanitization **Standard:** Sanitize user inputs in components to prevent cross-site scripting (XSS) attacks. **Do This:** * Use a sanitization library (e.g., DOMPurify) to sanitize user inputs before rendering them in the HTML templates. * Escape HTML entities in user inputs using Astro's built-in templating engine features. **Don't Do This:** * Render user inputs directly in the HTML templates without any sanitization or escaping. **Why:** Input sanitization prevents malicious code from being injected into the application, which can compromise user data and security. **Example:** """astro // Example using DOMPurify --- import DOMPurify from 'dompurify'; interface Props { userInput: string; } const { userInput } = Astro.props; // Sanitize the user input const sanitizedInput = DOMPurify.sanitize(userInput); --- <p set:html={sanitizedInput}></p> """ ### 4.2 Avoid Exposing Sensitive Data **Standard:** Avoid exposing sensitive data in client-side components. **Do This:** * Store sensitive data on the server-side. * Use environment variables to store API keys and other sensitive configuration values. Utilize the "import.meta.env" to access environment variables for the build **Don't Do This:** * Include API keys or other sensitive data directly in client-side components. * Expose sensitive data in the HTML source code. **Why:** Protecting sensitive data prevents unauthorized access and protects user privacy. **Example:** """astro --- // Access the API key from an environment variable const apiKey = import.meta.env.PUBLIC_API_KEY; --- // Use the API key to fetch data from the server """ ## 5. Testing ### 5.1 Unit Testing **Standard:** Write unit tests for individual components to ensure they function correctly. **Do This:** * Use a testing framework (e.g., Jest, Mocha) to write unit tests. * Test the component's props, state, and rendering behavior. * Use mocking to isolate components from external dependencies. **Don't Do This:** * Skip unit tests for complex components. * Write unit tests that are tightly coupled to the component's implementation details. **Why:** Unit tests improve code quality, prevent regressions, and make it easier to refactor code. ### 5.2 Component Integration tests **Standard:** Write integration tests to ensure components interact correctly in a larger system. **Do This:** * Use testing libraries (e.g., Playwright, Cypress) for end-to-end tests to verify that components render and function correctly in the browser. * Verify that data is passed to components in an intended manor. **Don't Do This:** * Neglect to test component collaboration. * Write only unit tests, and don't test real life rendering behavior. **Why:** Unit and Integration testing are essential for ensuring that components work in intended ways. ## 6. Documentation ### 6.1 Component Documentation **Standard:** Document component usage and functionality clearly. **Do This:** * Include a JSDoc-style comment block at the top of each component file that describes the component's purpose, props, and usage. * Use a documentation generator (e.g., Storybook) to create a living style guide for the component library. **Don't Do This:** * Skip documenting component usage and functionality * Make hard to maintain documentation. Keep documentation with the components themselves. **Why:** Documentation improves code understandability, facilitates collaboration, and makes it easier to maintain the component library. **Example:** """astro /** * A reusable button component. * * @param {string} text - The text to display on the button. * @param {'primary' | 'secondary'} variant - The button variant (optional, defaults to 'primary'). * @param {() => void} onClick - The function to call when the button is clicked (optional). */ interface Props { text: string; variant?: 'primary' | 'secondary'; onClick?: () => void; } """ These coding standards are a living document and should be updated as new versions of Astro are released and new best practices emerge. By following these standards, development teams can build high-quality, maintainable, and performant Astro applications.
# Core Architecture Standards for Astro This document outlines the core architectural standards for Astro projects. These standards are designed to promote maintainability, scalability, performance, and security. Following these guidelines will help ensure consistency across projects and facilitate collaboration among developers. This document assumes familiarity with Astro's fundamental concepts. Focus will be on using the newest features and avoiding legacy patterns. ## 1. Project Structure and Organization A well-defined project structure is crucial for maintainability and scalability. Consistency in how we organize our code lets us navigate any Astro project quickly. **Standards:** * **Do This:** Adhere to a modular and component-based architecture. Organize code into reusable components. * **Don't Do This:** Create monolithic structures that intertwine functionality, making components difficult to reuse and test. * **Why:** Modularity promotes separation of concerns, improving code readability, testability, and reusability. Component-based structure is fundamental to Astro's design. ### 1.1 "src/" Directory The "src/" directory is the heart of an Astro project, containing all the project's source code. **Standards:** * **Do This:** Structure the "src/" directory with the following subdirectories: * "components/": Reusable UI components (Astro components, React, Vue, Svelte, etc.). * "layouts/": Page layouts. * "pages/": Routes and page definitions. Each ".astro" file here represents a page. * "content/": Content collections for structured data (blog posts, documentation, etc.). * "utils/": Utility functions. * "styles/": Global styles and themes. * "scripts/": Client-side JavaScript. * **Don't Do This:** Place all code directly into the "src/" directory or create an unstructured mess. Also, avoid inconsistent naming conventions for directories. * **Why:** A predictable structure improves navigation and code discoverability. **Example:** """ astro-project/ ├── src/ │ ├── components/ │ │ ├── Card.astro │ │ └── Button.jsx │ ├── layouts/ │ │ ├── BaseLayout.astro │ │ └── BlogPostLayout.astro │ ├── pages/ │ │ ├── index.astro │ │ ├── about.astro │ │ └── blog/[slug].astro │ ├── content/ │ │ ├── blog/ │ │ │ ├── first-post.mdx │ │ │ └── second-post.mdx │ │ └── config.ts │ ├── utils/ │ │ ├── date-formatter.ts │ │ └── api-client.js │ ├── styles/ │ │ ├── global.css │ │ └── theme.css │ └── scripts/ │ └── app.js ├── astro.config.mjs ├── package.json └── tsconfig.json """ ### 1.2 Component Organization Consistent component structure and naming are essential. **Standards:** * **Do This:** * Use PascalCase for component filenames (e.g., "MyComponent.astro"). * Group related components in directories within "src/components/". * Create a "index.astro" or "index.jsx" file within component directories to export the main component, allowing for shorter import paths. * **Don't Do This:** Use inconsistent naming schemes or scatter components throughout the project. * **Why:** Improves readability and simplifies component imports. **Example:** """ src/components/ ├── Blog/ │ ├── BlogPostCard.astro │ ├── BlogPostList.astro │ └── index.astro // Exports BlogPostList & BlogPostCard └── UI/ ├── Button.jsx ├── Input.jsx └── index.jsx // Exports Button & Input """ **Import Example:** """astro --- import { BlogPostList } from '@components/Blog'; //Importing from the Blog component --- <BlogPostList /> """ ### 1.3 Content Collections Astro's content collections are a powerful way to manage structured content. **Standards:** * **Do This:** * Use content collections for blog posts, documentation, and other structured data. * Define schemas for your content collections using Zod to ensure data consistency. * Utilize the "getCollection()" function to query content within your components. * **Don't Do This:** Hardcode content directly into components or forgo defining schemas for collections. * **Why:** Provides data validation and type safety, improving content management. **Example:** "src/content/config.ts" """typescript import { defineCollection, z } from 'astro:content'; const blogCollection = defineCollection({ schema: z.object({ title: z.string(), date: z.date(), author: z.string(), draft: z.boolean().default(false), tags: z.array(z.string()).optional(), description: z.string(), image: z.string().optional() }), }); export const collections = { 'blog': blogCollection, }; """ "src/pages/blog/[slug].astro" """astro --- import { getCollection, getEntryBySlug } from 'astro:content'; import { format } from 'date-fns'; export async function getStaticPaths() { const blogPosts = await getCollection('blog'); return blogPosts.map((post) => ({ params: { slug: post.slug }, props: { post }, })); } const { post } = Astro.props; const { Content } = await post.render(); --- <html lang="en"> <head> <title>{post.data.title}</title> </head> <body> <article> <h1>{post.data.title}</h1> <p>Published on: {format(post.data.date, 'MMMM dd, yyyy')}</p> <Content /> </article> </body> </html """ ## 2. Component Design and Implementation Effective component design is fundamental to building maintainable Astro applications. **Standards:** * **Do This:** Follow the principles of single responsibility, separation of concerns, and loose coupling when designing components. * **Don't Do This:** Create overly complex components or ones with dependencies that are hard to manage. * **Why:** Facilitates code maintenance, testing, and reuse. ### 2.1 Astro Component Best Practices Astro components should be self-contained and reusable. **Standards:** * **Do This:** * Use props to pass data into components. Use prop destructuring for improved readability. * Utilize the client directives ("client:load", "client:idle", "client:visible", "client:only") to optimize JavaScript loading and execution. SSR first. * Use Slots to allow users of your components to inject HTML and other components. * **Don't Do This:** Directly modify global state within components or tightly couple components to specific data sources. * **Why:** Promotes component reusability and testability. **Example:** """astro --- // src/components/Card.astro interface Props { title: string; body: string; href: string; } const { title, body, href } = Astro.props; --- <a href={href} class="card"> <h3> {title} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> </svg> </h3> <p>{body}</p> </a> <style> .card { /* Styles... */ } </style> """ """astro // src/pages/index.astro --- import Card from '../components/Card.astro'; --- <Card title="Astro" body="Learn more about Astro" href="https://astro.build/" /> """ ### 2.2. Using Client Directives Astro's client directives control when and how JavaScript is loaded and executed. **Standards:** * **Do This:** * Use "client:load" for components that need to be interactive as soon as possible. * Use "client:idle" for components that can wait until the browser is idle. * Use "client:visible" for components that should load when they become visible in the viewport. * Use "client:only" *sparingly* and only when necessary for UI frameworks that are not server-renderable at all. * Default to server-side rendering when interactivity isn't required. * **Don't Do This:** Load all JavaScript eagerly using "client:load" if it is not necessary (hurts performance). * **Why:** Optimizes page load performance by deferring the loading of non-critical JavaScript. **Example:** """jsx // src/components/Counter.jsx import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } """ """astro // src/pages/index.astro --- import Counter from '../components/Counter.jsx'; --- <Counter client:idle /> """ ### 2.3 Styling Components Use modular and maintainable styling approaches. **Standards:** * **Do This:** * Use CSS modules, styled components, or utility-first CSS frameworks (Tailwind CSS) for component-specific styling. * Use global CSS files ("src/styles/global.css") for base styles and theming. * Leveraging Astro's built-in CSS scoping for ".astro" components. * **Don't Do This:** Write inline styles directly into components (except for highly specific, dynamic cases) or pollute the global namespace with un-scoped CSS. * **Why:** Improves style isolation, reusability, and maintainability. **Example (CSS Modules):** """jsx // src/components/Button.jsx import styles from './Button.module.css'; export default function Button({ children, onClick }) { return ( <button className={styles.button} onClick={onClick}> {children} </button> ); } """ """css /* src/components/Button.module.css */ .button { background-color: #007bff; color: white; padding: 10px 20px; border: none; cursor: pointer; } .button:hover { background-color: #0056b3; } """ **Example (Tailwind CSS):** """jsx // src/components/Button.jsx export default function Button({ children, onClick }) { return ( <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={onClick}> {children} </button> ); } """ **Example (Astro Component scoping):** """astro --- // src/components/MyComponent.astro --- <div class='container'> <p>Styled paragraph</p> </div> <style> .container { border: 1px solid red; } p { color: blue; } </style> """ The CSS rules are scoped to the "<MyComponent>" component. ## 3. Data Fetching and Management Efficient data fetching and management are critical for performance. **Standards:** * **Do This:** Use Astro's built-in "fetch()" API or other data-fetching libraries (e.g., "axios", "ky") to retrieve data. Use content collections for local data. Implement caching strategies to avoid unnecessary requests. * **Don't Do This:** Perform data fetching directly within client-side components (except when absolutely necessary) or neglect implementing caching. * **Why:** Improves performance and reduces network load. ### 3.1 Server-Side Data Fetching Where possible, fetch data on the server. **Standards:** * **Do This:** Fetch data within "getStaticPaths()" or directly in your Astro component's frontmatter when possible. Use environment variables to securely store API keys. * **Don't Do This:** Expose API keys directly in your client-side code. * **Why:** Prevents exposing sensitive data to the client and improves security. **Example:** """astro --- // src/pages/index.astro import { getPosts } from '../utils/api-client'; const posts = await getPosts(); --- <ul> {posts.map((post) => ( <li>{post.title}</li> ))} </ul> """ """javascript // src/utils/api-client.js const API_URL = import.meta.env.API_URL; export async function getPosts() { const res = await fetch("${API_URL}/posts"); const data = await res.json(); return data; } """ ### 3.2 Caching Strategies Effective caching can significantly improve performance. **Standards:** * **Do This:** Utilize HTTP caching headers, service workers, or in-memory caching to store frequently accessed data. * **Don't Do This:** Neglect implementing caching mechanisms. * **Why:** Reduces network requests and improves response times. **Example (HTTP Caching):** Set appropriate "Cache-Control" headers on your API responses. """javascript //Example Node.js server app.get('/api/posts', (req, res) => { res.set('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour // code to fetch and send posts }); """ ## 4. Error Handling and Logging Robust error handling and logging are essential for tracking down bugs in production **Standards:** * **Do This:** Implement comprehensive error handling mechanisms using "try...catch" blocks and Astro's "onError" hook. Implement a logging strategy using a service like Sentry or a custom logging mechanism. * **Don't Do This:** Allow errors to go unhandled or neglect implementing logging. * **Why:** Improves application resilience and helps diagnose issues. ### 4.1 Error Boundaries Using error boundaries within components to catch errors and present fallback UI. Note that framework-specific boundaries ("React.ErrorBoundary", "Vue's onErrorCaptured") can be leveraged with Astro islands. **Example:** """jsx // src/components/ErrorBoundary.jsx (React Example) import React, { Component } from 'react'; class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { console.error("Caught error: ", error, errorInfo); } render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; } return this.props.children; } } export default ErrorBoundary; """ """astro // src/pages/index.astro --- import ErrorBoundary from '../components/ErrorBoundary.jsx'; import MyComponent from '../components/MyComponent.jsx'; --- <ErrorBoundary client:visible> <MyComponent client:visible /> </ErrorBoundary> """ ### 4.2 Logging Implement a comprehensive logging strategy. **Standards:** * **Do This:** Use a logging service like Sentry or implement a custom logging mechanism. Log errors, warnings, and informational messages. Use environment variables to configure logging levels. * **Don't Do This:** Log sensitive information or neglect implementing logging. * **Why:** Helps diagnose issues and track application health. **Example (Sentry):** """javascript // src/scripts/sentry.js import * as Sentry from "@sentry/browser"; Sentry.init({ dsn: import.meta.env.SENTRY_DSN, integrations: [ new Sentry.BrowserTracing(), new Sentry.Replay() ], // Performance Monitoring tracesSampleRate: 0.1, // Capture 10% of transactions for performance monitoring. // Session Replay replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to lower it at first. replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. }); """ """astro // src/layouts/BaseLayout.astro --- import '../scripts/sentry.js';//Initialize sentry in a base layout --- """ ## 5. Accessibility (a11y) Building accessible websites is a crucial aspect of inclusive design. **Standards:** * **Do This:** Follow accessibility guidelines like WCAG (Web Content Accessibility Guidelines). Use semantic HTML, provide alternative text for images, and ensure sufficient color contrast. Test your website with accessibility tools like Axe. * **Don't Do This:** Neglect accessibility considerations or create websites that are difficult for users with disabilities to navigate. * **Why:** Ensures that your website is usable by everyone, regardless of their abilities. Also, good for SEO. ### 5.1 Semantic HTML Use semantic HTML elements to provide structure and meaning to your content. **Standards:** * **Do This:** Use elements like "<article>", "<nav>", "<aside>", "<header>", "<footer>", "<main>", and "<section>" appropriately. * **Don't Do This:** Use generic "<div>" elements for everything. * **Why:** Improves accessibility and SEO. **Example:** """astro <header> <nav> <ul> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/blog">Blog</a></li> </ul> </nav> </header> <main> <article> <h1>My Blog Post</h1> <p>Content of the blog post.</p> </article> </main> <footer> <p>© 2024 My Website</p> </footer> """ ### 5.2 ARIA Attributes Use ARIA (Accessible Rich Internet Applications) attributes to enhance accessibility. **Standards:** * **Do This:** Use ARIA attributes to provide additional information to assistive technologies when semantic HTML is not sufficient. * **Don't Do This:** Overuse ARIA attributes or use them incorrectly. * **Why:** Improves accessibility for users with disabilities. **Example:** """jsx // src/components/CustomButton.jsx export default function CustomButton({ onClick, children, ariaLabel }) { return ( <button onClick={onClick} aria-label={ariaLabel}> {children} </button> ); } """ ### 5.3 Image Alt Text Provide alternative text for images to describe their content. **Standards:** * **Do This:** Add "alt" attributes to all "<img>" elements. The "alt" text should be concise and descriptive. * **Don't Do This:** Leave "alt" attributes empty or use generic descriptions like "image". * **Why:** Improves accessibility for users who cannot see the images. **Example:** """astro <img src="/images/my-image.jpg" alt="A beautiful sunset over the ocean" /> """ ## 6. Security Security is paramount. Adhere to security best practices to protect your application and users. **Standards:** * **Do This:** Sanitize user inputs, use HTTPS, protect against Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF) attacks. Use secure coding practices. * **Don't Do This:** Store sensitive information in client-side code or neglect implementing security measures. * **Why:** Protects your application and users from security vulnerabilities and attacks. ### 6.1 Input Sanitization Sanitize user inputs to prevent XSS attacks. **Standards:** * **Do This:** Use a library like "DOMPurify" to sanitize user inputs before rendering them in your Astro components. * **Don't Do This:** Directly render user inputs without sanitization. * **Why:** Prevents attackers from injecting malicious scripts into your application. **Example:** """javascript // src/utils/sanitize.js import DOMPurify from 'dompurify'; export function sanitize(html) { return DOMPurify.sanitize(html); } """ """astro --- // src/pages/index.astro import { sanitize } from '../utils/sanitize'; const userInput = "<script>alert('XSS');</script>Hello!"; const sanitizedInput = sanitize(userInput); --- <p set:html={sanitizedInput} /> """ ### 6.2 HTTPS Always use HTTPS to encrypt communication between the client and server. **Standards:** * **Do This:** Configure your server to use HTTPS. Obtain an SSL/TLS certificate from a trusted certificate authority. * **Don't Do This:** Use HTTP in production. * **Why:** Protects data in transit from eavesdropping and tampering. ### 6.3 Environment Variables Never directly expose sensitive information (API keys, database passwords, etc.) in your client-side code. **Standards:** * **Do This:** Store all sensitive information in environment variables. Access environment variables using "import.meta.env". Ensure ".env" files are not committed to source control. * **Don't Do This:** Hardcode sensitive information in your code directly. * **Why:** prevents sensitive information from being exposed. **Example:** """javascript // .env API_KEY=your_api_key DATABASE_URL=your_database_url """ """javascript // src/utils/api-client.js const API_KEY = import.meta.env.API_KEY; export async function fetchData() { const res = await fetch("/api/data?apiKey=${API_KEY}"); const data = await res.json(); return data; } """ These core architecture standards for Astro are designed to enable the creation of high-quality, maintainable, and scalable applications. Adherence to these standards will ensure consistency, improve collaboration, and contribute to the long-term success of Astro projects.
# Performance Optimization Standards for Astro This document outlines the coding standards and best practices for performance optimization within Astro projects. Following these guidelines will lead to faster, more responsive, and resource-efficient web applications. These standards are tailored to the latest Astro features and ecosystem. ## 1. Architectural Considerations Choosing the correct architecture and approach upfront is critical for performance. ### 1.1. Islands Architecture **Standard:** Leverage Astro's Islands Architecture to minimize JavaScript execution on the client. Each interactive component (island) should be isolated and loaded only when needed. **Why:** Islands Architecture reduces the amount of JavaScript that needs to be downloaded, parsed, and executed by the browser, leading to faster initial load times and improved Time to Interactive (TTI). **Do This:** * Identify genuinely interactive components that require client-side JavaScript. * Render static content as HTML without client-side hydration. * Use Astro's "client:" directives ("client:load", "client:visible", "client:idle", "client:only") to control when islands are loaded. **Don't Do This:** * Hydrate entire pages or large sections of static content with client-side JavaScript. * Load all JavaScript upfront, regardless of whether it's needed. * Use "client:load" for components that are initially below-the-fold. It will improve the initial paint, but delay the Time to Interactive. **Example:** """astro --- // MyComponent.astro --- <div class="my-component"> <p>This is a static paragraph.</p> <MyInteractiveComponent client:visible /> </div> """ """jsx // MyInteractiveComponent.jsx (or .tsx) import React, { useState } from 'react'; const MyInteractiveComponent = () => { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }; export default MyInteractiveComponent; """ **Anti-Pattern:** Hydrating the entire "MyComponent" Astro page just to make the button interactive, even though the paragraph is entirely static content. ### 1.2. Static Site Generation (SSG) **Standard:** Pre-render as much content as possible during build time (Static Site Generation). **Why:** SSG delivers static HTML files to the browser, eliminating server-side rendering overhead and resulting in faster page loads. **Do This:** * Fetch data and generate HTML pages during the build process for content that doesn't require real-time updates. * Use Astro's built-in support for Markdown and MDX to create content-rich static sites. **Don't Do This:** * Use Server-Side Rendering (SSR) unnecessarily for content that can be pre-rendered. * Make unoptimised network requests at runtime. **Example:** """astro --- // src/pages/blog/[slug].astro import { getEntryBySlug } from 'astro:content'; export async function getStaticPaths() { const blogEntries = await getCollection('blog'); return blogEntries.map(entry => ({ params: { slug: entry.slug }, props: { entry }, })); } const { entry } = Astro.props; const { Content } = await entry.render(); --- <Layout title={entry.data.title}> <h1>{entry.data.title}</h1> <Content /> </Layout> """ ### 1.3. Content Delivery Network (CDN) **Standard:** Utilize a CDN to serve static assets (images, CSS, JavaScript) from geographically distributed servers. **Why:** CDNs reduce latency by serving content from a server closer to the user's location. **Do This:** * Integrate Astro with a CDN provider like Cloudflare, Netlify CDN, or AWS CloudFront. * Configure your CDN to cache static assets effectively. **Don't Do This:** * Serve static assets directly from your origin server without caching. **Example:** While Astro doesn't directly configure your CDN; it provides a build output that's easily deployed to a CDN. Configure your deployment platform (Netlify, Vercel, Cloudflare Pages, etc.) to leverage its CDN capabilities. For example, on Netlify: 1. Deploy your Astro site to Netlify. 2. Netlify automatically uses its global CDN to serve your static assets. 3. Configure Cache-Control headers within Netlify's settings or within the "netlify.toml" file for detailed cache management. ### 1.4 Routing Strategy **Standard:** Adopt an optimised routing strategy. **Why:** An efficient routing approach ensures swift navigation and content delivery. **Do This:** * Employ dynamic routing for pages with changeable content. * Implement prefetching for anticipated navigation paths. * Strategically utilize client-side routing to achieve seamless transitions. **Don't Do This:** * Overcomplicate routing with excessive redirects. * Rely on solely server-side routing for every internal link. **Example:** """astro // src/pages/products/[id].astro export async function getStaticPaths() { const products = await fetchProductsFromDatabase(); return products.map(product => ({ params: { id: product.id }, })); } const { id } = Astro.params; const product = await fetchProductDetails(id); """ ## 2. Component Optimization Efficient components are critical to overall performance. ### 2.1. Minimize Client-Side JavaScript **Standard:** Reduce the amount of client-side JavaScript. **Why:** Too much JavaScript can significantly slow down page load times and degrade user experience, especially on mobile devices. **Do This:** * Use Astro components for the majority of your UI, which render to HTML by default. * Defer or lazy-load JavaScript only when necessary for interactivity. * Replace large libraries, with smaller, more efficient alternatives. **Don't Do This:** * Import entire JavaScript libraries when only a small portion is needed. * Rely on Client-Side-Rendering (CSR) when SSG or SSR is appropriate. * Use jQuery. **Example:** """astro --- // Bad: Importing the entire Lodash library // import _ from 'lodash'; // Good: Importing only the functions you need import debounce from 'lodash-es/debounce'; const debouncedFunction = debounce(() => { console.log('Debounced function called'); }, 300); --- <input type="text" on:input={debouncedFunction} /> """ ### 2.2. Efficient Data Fetching **Standard:** Optimize data fetching to minimize network requests and reduce data transfer. **Why:** Slow or inefficient data fetching can be a major performance bottleneck. **Do This:** * Use GraphQL or similar technologies to fetch only the data you need. * Implement caching strategies to avoid redundant requests. * Use Astro's "getStaticPaths" to pre-render pages at build time. * Utilize "Astro.fetchContent()" and similar mechanisms to hydrate components directly from Markdown or MDX. **Don't Do This:** * Make multiple unnecessary requests for the same data. * Fetch large amounts of data that are not needed on the page. **Example:** """astro --- // src/pages/blog.astro import { getCollection } from 'astro:content'; export async function getStaticProps() { const posts = await getCollection('blog'); return { props: { posts: posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()), }, }; } const { posts } = Astro.props; --- <ul> {posts.map((post) => ( <li> <a href={"/blog/${post.slug}"}>{post.data.title}</a> </li> ))} </ul> """ ### 2.3. Image Optimization **Standard:** Optimize images to reduce file size without sacrificing quality. **Why:** Large images can significantly slow down page load times. **Do This:** * Use optimized image formats like WebP or AVIF. * Resize images to the appropriate dimensions for their display size. * Use responsive images with the "<picture>" element or "srcset" attribute to serve different image sizes based on screen size. * Lazy load images using the "loading="lazy"" attribute. * Use Astro's built-in image optimization features or integrate with image optimization services. **Don't Do This:** * Use unnecessarily large images. * Serve images in unoptimized formats like BMP or TIFF. * Load all images immediately, even those that are below-the-fold. **Example:** """astro --- // Using Astro's built-in image optimization with the <Image /> component. Requires installing @astrojs/image. import { Image } from '@astrojs/image/components'; import myImage from '../assets/my-image.jpg'; --- <Image src={myImage} alt="My Image" width={600} height={400} format="webp" loading="lazy" /> """ ### 2.4 Component Reusability and Memoization **Standard:** Design components to be reusable and implement memoization techniques to prevent re-renders. **Why:** Reusable components reduce code duplication and improve maintainability. Memoization prevents unnecessary re-renders of components, improving performance. **Do This:** * Create reusable components with well-defined props. * Use "React.memo" or similar memoization techniques to prevent re-renders of pure components. * Use the "useMemo" and "useCallback" hooks in React to memoize expensive calculations and function references. **Don't Do This:** * Create tightly coupled components that are difficult to reuse. * Rely on global state excessively. * Re-render components unnecessarily when their props have not changed. **Example:** """jsx // React Component Memoization import React, { memo } from 'react'; const MyComponent = ({ data }) => { console.log('MyComponent rendered'); return ( <div> {data.map(item => ( <p key={item.id}>{item.name}</p> ))} </div> ); }; export default memo(MyComponent); """ """jsx // React useMemo and useCallback import React, { useState, useMemo, useCallback } from 'react'; function MyComponent({ items }) { const [count, setCount] = useState(0); const expensiveCalculation = useMemo(() => { console.log('Performing expensive calculation'); return items.reduce((sum, item) => sum + item.value, 0); }, [items]); const increment = useCallback(() => { setCount(c => c + 1); }, []); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <p>Expensive Calculation: {expensiveCalculation}</p> </div> ); } export default MyComponent; """ ## 3. Code Optimization Writing efficient code is essential in Astro projects. ### 3.1. Minimize DOM Manipulation **Standard:** Reduce direct DOM manipulation. **Why:** Direct DOM manipulations are resource-intensive. Minimize or batch them for increased performance. **Do This:** * Use Virtual DOM concepts such as those offered by Preact. Memoize components so that they are only re-rendered when required. * Batch updates where needed to prevent layout thrashing. **Don't Do This:** * Directly manage DOM elements where it can be avoided. **Example:** Instead of making individual changes, group them into a single update. While this isn't direct DOM manipulation (best practice!), the principle applies to how components render, and demonstrates the benefit of batching. """js // Update multiple styles at once element.style.cssText = " color: blue; font-size: 16px; font-weight: bold; "; """ ### 3.2. Efficient JavaScript **Standard:** Use efficient JavaScript code. **Why:** Inefficient JavaScript code can lead to performance bottlenecks. **Do This:** * Avoid memory leaks by properly cleaning up event listeners and timers. * Use efficient algorithms and data structures. * Profile your code to identify and optimize performance bottlenecks. * Use the "const" keyword for variables to indicate they will not be reassigned. * Use template literals instead of string concatenation for better readability and performance. * Use arrow functions for concise and efficient function expressions. * Use array methods liek "map", "filter", and "reduce" instead of imperative loops where appropriate. **Don't Do This:** * Write inefficient or unnecessarily complex code. * Use global variables excessively. * Ignore performance warnings from your IDE or linter. **Example:** """javascript // Efficiently calculate the sum of an array of numbers const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((acc, num) => acc + num, 0); console.log(sum); // Output: 15 """ ### 3.3. CSS Optimization **Standard:** Optimize CSS code for performance. **Why:** Inefficient CSS can impact rendering speed and performance. **Do This:** * Minify and compress CSS files. * Remove unused CSS rules. * Use CSS Modules or similar techniques to scope CSS rules and avoid conflicts. * Avoid overly complex CSS selectors. * Use CSS variables for reusable values. * Use "transform"property instead of "top", "right", "bottom", and "left" for animations. **Don't Do This:** * Write overly specific or nested CSS selectors. * Include large, unused CSS files. * Use inline styles excessively. **Example:** """css /* Minified CSS */ body{font-family:sans-serif;margin:0}h1{color:#333} """ ### 3.4 Font Optimization **Standard:** Optimize web fonts for performance. **Why:** Web fonts can significantly impact page load times if not properly optimized. **Do This:** * Use web font formats like WOFF2. * Load fonts asynchronously using "font-display: swap;". * Use font subsets to include only the characters needed on your site. * Host fonts locally to avoid third-party dependencies. **Don't Do This:** * Use too many custom fonts. * Load fonts synchronously, blocking rendering. * Use very large font files. **Example:** """css /* Load font asynchronously */ @font-face { font-family: 'MyFont'; src: url('/fonts/MyFont.woff2') format('woff2'); font-display: swap; } body { font-family: 'MyFont', sans-serif; } """ ## 4. Tooling and Automation Leverage tools and automation to enforce these standards. ### 4.1. Linting and Formatting **Standard:** Use linters and formatters to enforce code style and best practices. **Why:** Linters and formatters help maintain code consistency and identify potential issues early on. **Do This:** * Configure ESLint with recommended Astro and React rules. * Use Prettier to automatically format code. * Integrate linters and formatters into your development workflow using Git hooks or CI/CD pipelines. **Don't Do This:** * Ignore warnings and errors from linters. * Manually format code without using a formatter. **Example:** ".eslintrc.cjs" configuration """javascript module.exports = { env: { browser: true, es2020: true, node: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, plugins: ['react-refresh'], rules: { 'react-refresh/only-export-components': 'warn', '@typescript-eslint/no-unused-vars': 'warn', // Or "error" in stricter settings 'no-unused-vars': 'warn', 'no-console': 'warn', // Or "error" for production }, settings: { react: { version: 'detect', // Automatically detect the React version }, }, }; """ ### 4.2. Performance Monitoring **Standard:** Implement performance monitoring to track and improve performance over time. **Why:** Performance monitoring helps identify performance regressions and areas for improvement. **Do This:** * Use tools like Google PageSpeed Insights, WebPageTest, or Lighthouse to measure website performance. * Monitor real-user performance using tools like Google Analytics or New Relic. * Set up performance budgets to track and prevent performance regressions. **Don't Do This:** * Ignore performance metrics. * Rely solely on manual testing for performance evaluation. ### 4.3. Automated Testing **Standard:** Use automated tests to prevent performance regressions and ensure code quality. **Why:** Automated tests help catch performance issues early on and ensure that code changes do not negatively impact performance. **Do This:** * Write unit tests, integration tests, and end-to-end tests. * Use performance testing tools to measure the performance of your code. * Integrate tests into your CI/CD pipeline. **Don't Do This:** * Skip writing tests. * Rely solely on manual testing. ## 5. Security Considerations While this document primarily focuses on performance, security and performance are intertwined. ### 5.1. Secure Data Handling **Standard:** Always handle user data securely. **Why:** Securing user data protects privacy and prevents data breaches. **Do This:** * Sanitize and validate all user input to prevent cross-site scripting (XSS) attacks. * Use parameterized queries or ORMs to prevent SQL injection attacks. * Hash passwords using strong hashing algorithms like bcrypt or Argon2. * Use HTTPS to encrypt data in transit. **Don't Do This:** * Store sensitive data in plain text. * Trust user input without validation. ### 5.2 Dependency Management **Standard:** Regularly update dependencies to patch security vulnerabilities. **Why:** Outdated dependencies can contain security vulnerabilities that can be exploited by attackers. **Do This:** * Use a dependency management tool like npm or yarn to manage your project's dependencies. * Regularly update dependencies to the latest versions. * Use a tool like Snyk or Dependabot to automatically identify and fix security vulnerabilities in your dependencies. **Don't Do This:** * Use outdated dependencies. * Ignore security alerts from your dependency management tool. ## Conclusion By adhering to these performance optimization standards, Astro developers can create web applications that are fast, responsive, and resource-efficient. Continuously monitoring and improving performance is essential for delivering a great user experience. This document serves as a comprehensive guide that can be adopted by professional development teams as they build and maintain Astro projects. Continuously review and update these standards as the Astro ecosystem evolves.
# State Management Standards for Astro This document outlines coding standards for state management in Astro projects. These standards aim to promote maintainability, performance, and predictability of data flow throughout your Astro application. They are based on the current best practices for state management within Astro's architecture and ecosystem. ## 1. General Principles of State Management in Astro ### 1.1. Understanding Astro's Architecture and its Impact on State Astro is designed as a "content-focused" web framework. This means its primary goal is to deliver fast, static websites with islands of interactivity, rather than being a Single Page Application (SPA). This philosophy heavily influences how state management should be approached. * **Standard:** Favor build-time data fetching and static rendering wherever possible. * **Why:** Reduces client-side JavaScript, resulting in faster page loads and improved SEO. * **Do This:** Use "getStaticPaths()" and "Astro.glob()" to fetch data during the build process for routes and content. * **Don't Do This:** Fetch data dynamically on the client-side if it can be determined at build time. """astro // src/pages/blog/[slug].astro export async function getStaticPaths() { const posts = await Astro.glob('./posts/*.md'); return posts.map(post => ({ params: { slug: post.frontmatter.slug }, props: { post }, })); } const { post } = Astro.props; """ * **Standard:** Isolate interactive components ("islands") and manage their state independently. * **Why:** Astro's partial hydration model means that only the necessary components become interactive, limiting the scope of client-side state complexity. * **Do This:** Define clear boundaries for interactive components and use appropriate state management mechanisms within each island rather than trying to create one global state. * **Don't Do This:** Over-hydrate components unnecessarily or create a monolithic state object encompassing the entire application, undermining Astro's performance benefits. ### 1.2. Choosing the right State Management approach * **Standard:** Prefer component-local state for simple interactivity. * **Why:** Simplifies development, reduces dependencies, and aligns with Astro's component-centric architecture. * **Do This:** Utilize "useState" from React or similar hooks in your interactive components for basic UI updates and data handling when using a UI framework like React. * **Don't Do This:** Introduce a complex state management library when component-local state is sufficient. """jsx // src/components/Counter.jsx import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } export default Counter; """ * **Standard:** Use Context API (React or Vue) for sharing state within a component subtree. * **Why:** Provides a simple way to avoid prop drilling and share data between related components without external dependencies. * **Do This:** Create a Context provider higher up in the component tree and consume the context within child components that need access to the shared state. * **Don't Do This:** Use Context for global application state if more advanced features like state persistence, complex updates, or computed values are needed. """jsx // src/context/ThemeContext.jsx import React, { createContext, useState, useContext } from 'react'; const ThemeContext = createContext(); export function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } export function useTheme() { return useContext(ThemeContext); } // src/components/ThemeToggle.jsx import React from 'react'; import { useTheme } from '../context/ThemeContext'; function ThemeToggle() { const { theme, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme}> Toggle Theme (Current: {theme}) </button> ); } export default ThemeToggle; """ """astro // src/layouts/Layout.astro --- import { ThemeProvider } from '../context/ThemeContext.jsx'; --- <html lang="en"> <head>...</head> <body> <ThemeProvider> <slot /> </ThemeProvider> </body> </html> """ * **Standard:** Opt for a state management library (e.g., Zustand, Jotai, Recoil, Redux) only when dealing with complex application state, cross-component dependencies, or advanced data manipulation needs. * **Why:** Introduces additional complexity and overhead, so it should be justified by the specific requirements of the project. Consider the bundle size implications. * **Do This:** Carefully evaluate the pros and cons of different libraries and choose one that best suits the scale and complexity of your application. For simpler stores consider Zustand or Jotai. React Context + useReducer can be an alternative to Redux for many projects. * **Don't Do This:** Blindly adopt a state management library without considering whether simpler alternatives would suffice. ### 1.3. Data Fetching and API Interactions * **Standard:** Utilize Astro's "Astro.fetch()" for server-side data fetching. * **Why:** Integrates seamlessly with Astro's build process and offers performance optimizations. "Astro.fetch()" is a wrapper around the standard "fetch()" API, and can be used both at build time, and in API endpoints. * **Do This:** Fetch necessary data during the "getStaticPaths()" or within API routes. * **Don't Do This:** Rely solely on client-side fetching for data that could be fetched server-side. """astro // src/pages/posts.astro export async function getStaticPaths() { const response = await Astro.fetch('https://api.example.com/posts'); const posts = await response.json(); return posts.map(post => ({ params: { id: post.id }, props: { post }, })); } const { post } = Astro.props; """ * **Standard**: For client-side API calls, use "fetch" API with proper error handling and loading states. * **Why:** Provides a standard way to interact with APIs while maintaining a smooth user experience. * **Do This:** Display loading indicators while fetching data and handle potential errors gracefully. * **Example:** """jsx // src/components/UserList.jsx import React, { useState, useEffect } from 'react'; function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { const response = await fetch('/api/users'); // Fetch from an Astro API endpoint. if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } const data = await response.json(); setUsers(data); } catch (e) { setError(e); } finally { setLoading(false); } }; fetchData(); }, []); if (loading) return <p>Loading users...</p>; if (error) return <p>Error: {error.message}</p>; return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } export default UserList; // src/pages/api/users.js export async function get() { try { const users = [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}]; // Replace with actual data fetching from a database or external API. return new Response( JSON.stringify(users), { status: 200, headers: { "Content-Type": "application/json" } } ); } catch (error) { console.error(error); return new Response( JSON.stringify({ message: 'Failed to fetch users' }), { status: 500, headers: { "Content-Type": "application/json" } } ); } } """ ## 2. Specific State Management Patterns ### 2.1. Component-Local State with Hooks * **Standard:** Use "useState", "useReducer", "useContext" (from React or Vue) for managing state within individual components. * **Why:** Enables simple reactivity and isolates component logic. * **Do This:** Initialize state with appropriate default values and provide clear update functions. """jsx // src/components/Toggle.jsx import React, { useState } from 'react'; function Toggle() { const [isOn, setIsOn] = useState(false); const toggle = () => { setIsOn(!isOn); }; return ( <div> <button onClick={toggle}> {isOn ? 'Turn Off' : 'Turn On'} </button> <p>Status: {isOn ? 'ON' : 'OFF'}</p> </div> ); } export default Toggle; """ * **Standard:** Avoid deeply nested or complex state objects for local component state. * **Why:** Simplifies updates and reduces the risk of unintended side effects. * **Do This:** Break down complex state into multiple, smaller state variables. ### 2.2. Context API for Shared State * **Standard:** Create custom hooks to consume context values. * **Why:** Improves code readability and avoids repetitive "useContext" calls. It also encapsulates the context usage for cleaner code. * **Do This:** Create a "use[ContextName]" hook that returns the context value or specific properties. """jsx // src/context/AuthContext.jsx import React, { createContext, useState, useContext } from 'react'; const AuthContext = createContext(); export function AuthProvider({ children }) { const [user, setUser] = useState(null); const login = (userData) => { setUser(userData); }; const logout = () => { setUser(null); }; return ( <AuthContext.Provider value={{ user, login, logout }}> {children} </AuthContext.Provider> ); } export function useAuth() { return useContext(AuthContext); } // src/components/Profile.jsx import React from 'react'; import { useAuth } from '../context/AuthContext'; function Profile() { const { user, logout } = useAuth(); if (!user) { return <p>Please log in.</p>; } return ( <div> <p>Welcome, {user.name}!</p> <button onClick={logout}>Logout</button> </div> ); } export default Profile; """ * **Standard:** Limit the scope of Context providers to the components that need access to the shared state. * **Why:** Avoids unnecessary re-renders and maintains performance. Wrap only the components that need the context. * **Don't Do This:** Wrap the entire application in a single Context provider if only a small part of the app interacts with shared state. ### 2.3. External State Management Libraries (Zustand, Jotai, Recoil, Redux) * **Standard:** Use a state management library only when the complexity of the application warrants it. * **Why:** Introduces additional dependencies and complexity. * **Do This:** Carefully consider the trade-offs between simplicity and flexibility. * **Standard:** Follow the recommended patterns and best practices of the chosen library. * **Why:** Ensures maintainability and avoids common pitfalls * **Standard:** Consider using lightweight state management libraries like Zustand or Jotai for simple global state. * **Why:** They offer a simpler API and smaller bundle size compared to Redux. #### 2.3.1 Zustand Example """jsx // store.js (Zustand) import create from 'zustand'; const useStore = create(set => ({ bears: 0, increasePopulation: () => set(state => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })); export default useStore; // src/components/BearCounter.jsx import React from 'react'; import useStore from '../store'; function BearCounter() { const bears = useStore(state => state.bears); return <h1>{bears} bears around here!</h1>; } export default BearCounter; // src/components/Controls.jsx import React from 'react'; import useStore from '../store'; function Controls() { const increasePopulation = useStore(state => state.increasePopulation); const removeAllBears = useStore(state => state.removeAllBears); return ( <> <button onClick={increasePopulation}>one up</button> <button onClick={removeAllBears}>remove all</button> </> ); } export default Controls """ ## 3. Anti-Patterns and Common Mistakes * **Anti-Pattern:** Over-reliance on global state for everything. * **Why:** Makes the application harder to reason about, test, and maintain. Can also hurt performance due to excessive re-renders. * **Do This:** Prefer component-local state or context when appropriate. Restrict global state to data that truly needs to be accessed across the entire application. * **Anti-Pattern:** Mutating state directly. * **Why:** Can lead to unexpected side effects and inconsistencies. * **Do This:** Always update state immutably using spread syntax or appropriate update functions provided by the state management library. """jsx // Incorrect: const myState = { items: [1, 2, 3] }; myState.items.push(4); // Direct mutation! // Correct: const myState = { items: [1, 2, 3] }; const newState = { ...myState, items: [...myState.items, 4] }; // Immutable update """ * **Anti-Pattern:** Unnecessary re-renders due to state updates. * **Why:** Hurts performance. * **Do This:** Use "useMemo" and "useCallback" hooks (or equivalent techniques for other UI frameworks) to prevent unnecessary re-renders of components that depend on unchanged state values. Also carefully consider the granularity of your state updates. * **Mistake:** Forgetting to handle loading and error states when fetching data. * **Why:** Degrades user experience. * **Do This:** Display loading indicators and handle potential errors gracefully. * **Mistake** Using client:load or client:visible directives unnecessarily in favor of server-side rendering. * **Why:** Defeats the purpose of Astro by hydrating the components client-side unnecessarily and potentially hurting performance. * **Do This:** Only use these directives when specific client-side interactions or functionality are required and cannot be achieved server-side. ## 4. Security Considerations * **Standard:** Sanitize data received from external sources before storing it in state. * **Why:** Prevents XSS (Cross-Site Scripting) vulnerabilities. * **Do This:** Use appropriate sanitization libraries or techniques recommended by your JavaScript framework (e.g., DOMPurify, "escape-html"). * **Standard:** Avoid storing sensitive information (e.g., API keys, passwords) in client-side state. * **Why:** Exposes sensitive data to potential attackers. * **Do This:** Store sensitive information securely on the server and access it through API endpoints that require authentication. ## 5. Testing State Management * **Standard:** Write unit tests for state update logic, especially when using complex state management libraries. * **Why:** Ensures the correctness and predictability of state transitions. * **Do This:** Use testing frameworks like Jest or Mocha with libraries like React Testing Library to test your state management logic in isolation. * **Standard:** Test component interactions that trigger state updates. * **Why:** Verifies that the UI correctly interacts with and reflects changes in the application state. * **Do This:** Use integration tests or end-to-end tests to simulate user interactions and verify the resulting state changes. ## 6. Tooling and Automation * **Standard:** Utilize linters and code formatters (e.g., ESLint, Prettier) to enforce consistent coding style and best practices related to state management. * **Why:** Automates code quality checks and reduces the risk of errors. * **Do This:** Configure your linter and formatter with rules that enforce immutability, proper error handling, and other state management best practices. * **Standard:** Use TypeScript to add static typing to your state management code. * **Why:** Helps catch type-related errors early in the development process and improves code maintainability. * **Do This:** Define clear type definitions for your state objects and actions.
# Testing Methodologies Standards for Astro This document provides coding standards for testing methodologies in Astro projects, emphasizing unit, integration, and end-to-end (E2E) testing strategies tailored for the Astro framework. It outlines best practices to ensure robust, maintainable, and reliable Astro applications. ## 1. General Testing Principles ### 1.1. Importance of Testing **Do This:** Prioritize writing tests for all components, utilities, and integrations. **Don't Do This:** Neglect writing tests or postpone them until the end of the development cycle. **Why:** Testing ensures code correctness, facilitates easier refactoring, reduces bugs in production, and improves overall code quality. Investing in testing saves time and resources in the long run by preventing costly fixes and downtime. ### 1.2. Test-Driven Development (TDD) **Do This:** Consider adopting TDD where appropriate, writing tests before implementing the actual code. **Don't Do This:** Avoid TDD principles altogether, especially for complex features or critical functionalities. **Why:** TDD helps to clarify requirements, promote cleaner code design, and ensure comprehensive test coverage from the outset. ### 1.3. Testing Pyramid **Do This:** Strive for a well-balanced testing pyramid: * A large base of unit tests. * A substantial layer of integration tests. * A smaller layer of end-to-end tests. **Don't Do This:** Over-rely on E2E tests at the expense of unit and integration tests. **Why:** Unit tests are fast and isolate problems effectively. Integration tests verify interactions between components. E2E tests, while valuable, are slower and more brittle. A balanced pyramid optimizes testing efficiency and coverage. ### 1.4. Code Coverage **Do This:** Aim for high code coverage (e.g., 80% or higher), but focus more on testing critical paths and functionality effectively. **Don't Do This:** Blindly chase 100% code coverage without considering the quality and relevance of the tests. **Why:** Code coverage metrics provide insights into which parts of the code are tested. High coverage reduces the risk of undetected bugs, but meaningful tests are more important than just achieving a specific percentage. ## 2. Unit Testing ### 2.1. Purpose **Do This:** Use unit tests to verify the functionality of individual components, functions, or modules in isolation. **Don't Do This:** Write unit tests that depend on external resources or other components; use mocks or stubs instead. **Why:** Unit tests should be fast, reliable, and focus on the smallest testable units of code. ### 2.2. Tools **Do This:** Utilize modern testing frameworks such as Jest or Vitest. **Don't Do This:** Use outdated or unmaintained testing libraries. **Why:** Modern testing frameworks offer features like mocking, spying, code coverage, and parallel test execution, improving test efficiency and developer experience. Vitest is particularly well-suited for Astro projects as it's built on Vite, like Astro itself. ### 2.3. Example: Testing an Astro Component """astro --- // src/components/MyComponent.astro export interface Props { title: string; } const { title } = Astro.props; --- <h1>{title}</h1> """ """javascript // src/components/MyComponent.test.js (using Vitest) import { render, screen } from '@testing-library/astro'; import MyComponent from './MyComponent.astro'; import { describe, it, expect } from 'vitest'; describe('MyComponent', () => { it('should render the title correctly', async () => { const { container } = await render(MyComponent, { title: 'Hello, Astro!' }); expect(screen.getByText('Hello, Astro!')).toBeInTheDocument(); }); it('should have the correct HTML structure', async () => { const {container} = await render(MyComponent, {title: 'Testing!'}); expect(container.querySelector('h1')).toBeInTheDocument(); expect(container.querySelector('h1')?.textContent).toBe('Testing!'); }); }); """ **Explanation:** * **"@testing-library/astro"**: Used for rendering and querying Astro components. * **"render"**: Renders the component with the provided props. * **"screen"**: Provides methods for querying the rendered output. * **"expect"**: Used for making assertions. * We verify that the component renders the title prop correctly and that the HTML structure is as expected. ### 2.4. Example: Testing a Utility Function """javascript // src/utils/formatDate.js export function formatDate(dateString) { const date = new Date(dateString); const options = { year: 'numeric', month: 'long', day: 'numeric' }; return date.toLocaleDateString(undefined, options); } """ """javascript // src/utils/formatDate.test.js (using Vitest) import { formatDate } from './formatDate'; import { describe, it, expect } from 'vitest'; describe('formatDate', () => { it('should format the date correctly', () => { const dateString = '2024-01-01'; const formattedDate = formatDate(dateString); expect(formattedDate).toBe('January 1, 2024'); }); it('should handle invalid dates gracefully', () => { const dateString = 'invalid-date'; const formattedDate = formatDate(dateString); expect(formattedDate).toBe('Invalid Date'); // Or handle as appropriate }); }); """ **Explanation:** * We test that the utility function formats a valid date string correctly. * We also test how the function handles an invalid date, ensuring it doesn't throw an error and returns an appropriate value. ### 2.5. Mocks and Stubs **Do This:** Use mocks and stubs to isolate the component or function being tested from its dependencies. **Don't Do This:** Directly import and use real dependencies in unit tests. **Why:** Mocks and stubs allow you to control the behavior of dependencies, making tests more predictable and preventing external factors from affecting the test results. """javascript // Example using Jest mocks // src/components/ExternalAPIComponent.js async function fetchData() { const response = await fetch('https://api.example.com/data'); return response.json(); } export default function ExternalAPIComponent() { const data = await fetchData(); return <div>{data.value}</div>; } """ """javascript // src/components/ExternalAPIComponent.test.js import ExternalAPIComponent from './ExternalAPIComponent.js'; import { describe, it, expect, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/astro'; global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve({ value: 'Mocked Data' }), }) ); describe('ExternalAPIComponent', () => { it('should render data from the external API', async () => { const { container } = await render(ExternalAPIComponent); await waitFor(() => { expect(screen.getByText('Mocked Data')).toBeInTheDocument(); }); }); }); """ **Explanation:** * We mock the "global.fetch" function to return a predefined response. * This ensures that the test doesn't rely on the actual external API being available and stable. * The test verifies that the component renders the mocked data correctly. ## 3. Integration Testing ### 3.1. Purpose **Do This:** Use integration tests to verify that different parts of the application work together correctly. **Don't Do This:** Treat integration tests as unit tests or E2E tests. Integration tests should focus on the interactions between components or modules. **Why:** Integration tests catch issues that unit tests might miss, such as incorrect data flow or misconfigured dependencies. ### 3.2. Tools **Do This:** Use the same testing framework as unit tests (e.g., Jest or Vitest) or specialized integration testing tools like Playwright or Cypress. **Don't Do This:** Avoid using tools that are difficult to set up or maintain. **Why:** Using consistent tools simplifies the testing process and reduces the learning curve. ### 3.3. Example: Testing Component Interaction """astro // src/components/ParentComponent.astro import ChildComponent from './ChildComponent.astro'; export interface Props { data: string; } const { data } = Astro.props; --- <div> <ChildComponent message={data} /> </div> """ """astro // src/components/ChildComponent.astro export interface Props { message: string; } const { message } = Astro.props; --- <p>{message}</p> """ """javascript // src/components/ParentComponent.test.js (using Vitest) import { render, screen } from '@testing-library/astro'; import ParentComponent from './ParentComponent.astro'; import { describe, it, expect } from 'vitest'; describe('ParentComponent', () => { it('should render the ChildComponent with the correct message', async () => { const { container } = await render(ParentComponent, { data: 'Hello from Parent!' }); expect(screen.getByText('Hello from Parent!')).toBeInTheDocument(); }); }); """ **Explanation:** * We test that the "ParentComponent" correctly passes the "data" prop to the "ChildComponent". * The test verifies that the "ChildComponent" renders the message as expected. ### 3.4. Mocking External Services in Integration Tests **Do This:** Mock external dependencies in integration tests where possible to avoid relying on live services. **Don't Do This:** Skip mocking essential services, as inconsistent external behavior can make your tests unreliable. """javascript // Example mocking an API call in an integration test // src/services/apiService.js export async function fetchDataFromAPI() { const response = await fetch('/api/data'); return await response.json(); } """ """javascript // src/components/DataComponent.astro import { fetchDataFromAPI } from '../services/apiService'; async function getData() { return await fetchDataFromAPI(); } const data = await getData(); --- <div>{data?.message}</div> """ """javascript // src/components/DataComponent.test.js import { render, screen, waitFor} from '@testing-library/astro'; import DataComponent from './DataComponent.astro'; import { fetchDataFromAPI } from '../services/apiService'; import { describe, it, expect, vi } from 'vitest'; vi.mock('../services/apiService', () => ({ fetchDataFromAPI: vi.fn(() => Promise.resolve({ message: 'Mocked API data' })) })); describe('DataComponent', () => { it('should render data from the API service', async () => { const { container } = await render(DataComponent); await waitFor(() => { expect(screen.getByText('Mocked API data')).toBeInTheDocument(); }); expect(fetchDataFromAPI).toHaveBeenCalled(); }); }); """ **Explanation:** * We mock the "fetchDataFromAPI" function using "vi.mock". * This prevents the integration test from making actual API calls. * We then assert that the mocked method has been called, implying the DataComponent is using it. ## 4. End-to-End (E2E) Testing ### 4.1. Purpose **Do This:** Use E2E tests to verify that the entire application works correctly from the user's perspective. **Don't Do This:** Rely solely on E2E tests; they are slower and more brittle than unit and integration tests. **Why:** E2E tests simulate real user interactions and catch integration issues that other testing types might miss. ### 4.2. Tools **Do This:** Choose E2E testing tools like Playwright or Cypress. **Don't Do This:** Use tools that are difficult to set up, configure, or maintain. **Why:** Playwright and Cypress offer features like automatic waiting, cross-browser testing, and detailed error reporting, improving developer productivity. Playwright is particularly well-suited for Astro due to its modern architecture and excellent TypeScript support. ### 4.3. Example: Testing a Navigation Flow """javascript // playwright.config.js (Playwright configuration file) import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', fullyParallel: true, reporter: 'html', use: { baseURL: 'http://localhost:3000', // Replace with your development server URL trace: 'on-first-retry', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], webServer: { command: 'npm run dev', // Replace with your development server command port: 3000, reuseExistingServer: !process.env.CI, }, }); """ """javascript // tests/navigation.spec.js (Playwright test file) import { test, expect } from '@playwright/test'; test('navigation to the about page', async ({ page }) => { await page.goto('/'); await page.getByRole('link', { name: 'About' }).click(); await expect(page).toHaveURL('/about'); await expect(page.getByRole('heading', { name: 'About Us' })).toBeVisible(); }); test('check title', async ({ page }) => { await page.goto('https://example.com/'); // Expect a title "to contain" a substring. await expect(page).toHaveTitle(/Example Domain/); }); """ **Explanation:** * We use Playwright to navigate to the homepage ("/") and then click on the "About" link. * We assert that the page URL changes to "/about" and that the "About Us" heading is visible. This verifies the basic navigation flow. ### 4.4. Writing Reliable E2E Tests **Do This:** Write E2E tests that are resilient to minor UI changes and timing issues. **Don't Do This:** Write brittle tests that are prone to failure due to small UI adjustments or network latency. **Why:** Robust E2E tests are crucial for maintaining a stable and reliable application. Avoid using fixed timeouts, and instead favor locating elements by their role/label. ### 4.5. Environment Variables **Do This**: Store environment-specific configuration values such as base URLs in ".env" files and use environment variables to configure your tests. **Don't Do This**: Hardcode URLs or API keys directly in your tests. **Why**: Using environment variables ensures that your tests can be run in different environments (e.g., development, staging, production) without requiring code changes. ## 5. Accessibility Testing. ### 5.1 Importance of Accessibility **Do This:** Implement accessibility checks in your tests to ensure your application is usable by everyone. **Don't Do This:** Neglect accessibility testing, as it can exclude users and lead to legal issues. **Why:** Accessible applications provide a better user experience for everyone, including users with disabilities. ### 5.2 Tools **Do This**: Use accessibility testing tools like axe-core along with testing frameworks. **Don't Do This**: Depend solely on manual checks without automated testing. **Why**: Automated accessibility testing can catch common accessibility issues early in the development process. ### 5.3 Example Accessibility test """javascript // tests/e2e/accessibility.spec.js import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test.describe('Accessibility', () => { test('homepage should not have any automatically detectable accessibility issues', async ({ page }) => { await page.goto('/'); const axeBuilder = new AxeBuilder({ page }); const accessibilityScanResults = await axeBuilder.analyze(); expect(accessibilityScanResults.violations).toEqual([]); }); }); """ ## 6. Performance Testing ### 6.1 Importance of Performance Testing **Do This:** Measure the performance of your Astro application using appropriate tools and techniques. **Don't Do This:** Ignore performance testing, as slow applications can lead to poor user experience and loss of business. **Why:** Performance testing helps identify bottlenecks and areas for optimization in your application. ### 6.2 Tools **Do This:** Use performance testing tools like Lighthouse, WebPageTest, or browser developer tools. **Don't Do This:** Rely solely on manual observations without quantitative metrics. **Why:** These tools provide detailed insights into various performance metrics, such as load time, rendering time, and resource utilization. ## 7. Common Anti-Patterns to Avoid ### 7.1. Flaky Tests **Don't Do This:** Accept flaky tests (tests that pass sometimes and fail sometimes). **Do This:** Investigate and fix flaky tests; they undermine confidence in the test suite. **Why:** Flaky tests often indicate underlying issues in the code or test setup. ### 7.2. Over-mocking **Don't Do This:** Mock everything in sight. **Do This:** Mock only the dependencies that are necessary to isolate the unit under test. **Why:** Over-mocking can lead to tests that don't accurately reflect the behavior of the real system. ### 7.3. Ignoring Test Coverage **Don't Do This:** Ignore code coverage metrics. **Do This:** Use code coverage reports to identify areas of the code that are not adequately tested. **Why:** Code coverage helps to ensure that all parts of the code are covered by tests. ### 7.4. Hardcoded Values **Don't Do This:** Hardcode values in tests. **Do This:** Use test data generators or fixtures to create realistic and maintainable test data. **Why:** Hardcoded values make tests brittle and difficult to maintain. By adhering to these coding standards, you can ensure that your Astro projects are well-tested, maintainable, and reliable. Remember to continuously review and update these standards as the Astro framework and testing tools evolve.