Data Fetching Patterns
This guide documents our established patterns for data fetching, using real examples from our codebase. For detailed GraphQL patterns, see GraphQL Query Patterns.
Client-Side Data Fetching
Section titled “Client-Side Data Fetching”API Routes
Section titled “API Routes”Use API routes to protect sensitive data and proxy requests:
import { typesenseClient } from "#utils/typesense";
export async function GET({ request }) { try { const url = new URL(request.url); const query = url.searchParams.get("q");
const results = await typesenseClient.search(query); return new Response(JSON.stringify(results)); } catch (err) { console.error("Search failed", { error: err.message }); return new Response(JSON.stringify({ error: "Search failed" }), { status: 500, }); }}Form Submissions
Section titled “Form Submissions”For form handling patterns, see our detailed Forms and User Input Patterns guide.
Server-Side Data Fetching
Section titled “Server-Side Data Fetching”GraphQL in Astro Pages
Section titled “GraphQL in Astro Pages”For detailed GraphQL patterns, including leaf node architecture and fragment composition, see GraphQL Query Patterns.
Page-Level Data Fetching
Section titled “Page-Level Data Fetching”Use GraphQL in Astro pages for server-side data fetching:
import { homepageAPI } from "./_homepage/graphql/api"; const data = await homepageAPI();if (!data) { return Astro.rewrite("/404"); } const { title, metaTags, hero, featuredDestinations} = data;
<Layout title={title} canonicalUrl={`${LP_URL}`} metadata={metaTags}> <Hero3Up data={hero} /> <FeaturedDestinations data={featuredDestinations} /></Layout>🤔 Why This Pattern?
- Server-side data fetching
- Validate minimum required data
- Early error handling
- Type-safe data access
GraphQL API Organization
Section titled “GraphQL API Organization”Structure your GraphQL files by feature:
pages/articles/[slug]/├── _graphql/│ ├── api.ts # API functions│ ├── article.types.ts # Type definitions│ ├── page.query.graphql # Main query│ ├── transform.ts # Data transformations│ └── validate.query.graphql # Validation queryExample API implementation:
import { graphqlApi } from "#graphql/graphqlClient";import ARTICLE_BY_SLUG from "./page.query.graphql";import type { Article } from "./article.types";import transform from "./transform";
export async function getArticleBySlug(slug: string) { try { const { data } = await graphqlApi({ query: ARTICLE_BY_SLUG, variables: { slug }, });
// Check for minimum required data if (!data?.article?.content || !data?.article?.title) { console.error("Missing required article data", { component: "getArticleBySlug", slug, data: { hasContent: Boolean(data?.article?.content), hasTitle: Boolean(data?.article?.title), }, }); return null; }
return transform(data); } catch (err) { console.error("Failed to fetch article", { error: err.message, component: "getArticleBySlug", slug, }); return null; }}🤔 Why This Pattern?
- Organized by feature
- Type-safe data handling
- Validate required fields
- Clean separation of concerns
Best Practices for Form Actions
Section titled “Best Practices for Form Actions”-
Action Definition
- Define schemas for validation
- Provide meaningful error messages
- Handle all error cases
- Return consistent response shapes
-
Form Implementation
- Use HTML forms for simple cases
- Use SolidJS for complex forms
- Show loading states
- Provide clear feedback
-
Error Handling
- Validate input server-side
- Return structured errors
- Show user-friendly messages
- Log errors appropriately
-
Security
- Validate all inputs
- Sanitize data
- Rate limit submissions
- Prevent CSRF attacks
Anti-Patterns to Avoid
Section titled “Anti-Patterns to Avoid”❌ Don’t: Skip server-side validation
// Bad: Client-only validationconst handler = ({ email }) => { // No validation before use return subscribeUser(email);};✅ Do: Always validate server-side
// Good: Server-side validationconst handler = ({ email }) => { const result = schema.safeParse({ email }); if (!result.success) { return { error: result.error }; } return subscribeUser(result.data.email);};❌ Don’t: Use raw fetch for form submission
// Bad: Manual fetch implementationfetch("/api/subscribe", { method: "POST", body: JSON.stringify({ email }),});✅ Do: Use Astro actions
// Good: Type-safe action callsconst result = await actions.newsletter({ email: email(),});Decision Checklist
Section titled “Decision Checklist”When implementing forms, ask:
-
Form Complexity
- Is JavaScript required?
- Are there complex interactions?
- Is real-time validation needed?
-
Data Handling
- Is input validation defined?
- Are all error cases handled?
- Is feedback provided?
-
User Experience
- Are loading states shown?
- Is error feedback clear?
- Is success feedback shown?
-
Security
- Is data validated?
- Are inputs sanitized?
- Is rate limiting implemented?
Handling Partial Data
Section titled “Handling Partial Data”GraphQL allows partial data returns. Handle these cases gracefully:
---import { destinationAPI } from "./_graphql/api";
const data = await destinationAPI(Astro.params.slug);
if (!data) { return Astro.rewrite("/404");}
// Transform data for componentsconst { overview, highlights, experiences, places } = data.destination;---
<Layout> {overview && <Overview data={overview} />} {highlights && <Highlights data={highlights} />} {experiences && <Experiences data={experiences} />} {places && <Places data={places} />}</Layout>🤔 Why This Pattern?
- Clear content requirements
- Graceful fallbacks
- Structured error logging
- Type-safe data handling
Best Practices
Section titled “Best Practices”-
Server-Side Data Fetching
- Use Astro’s server-side data fetching by default
- Define and validate minimum content requirements
- Transform data before sending to components
- Handle errors early and log appropriately
-
Client-Side Data Fetching
- Use API routes to proxy requests and protect secrets
- Consider Astro actions for form submissions
- Implement proper loading and error states
- Keep sensitive data server-side
-
Error Handling
- Log structured errors with context
- Return appropriate status codes (404/500)
- Provide user-friendly error messages
- Include error recovery options
-
Data Validation
- Define minimum content requirements
- Check for required fields
- Handle partial data gracefully
- Log missing data appropriately
Anti-Patterns to Avoid
Section titled “Anti-Patterns to Avoid”❌ Don’t: Expose API keys in client-side code
// Bad: Exposing API keyconst fetchData = () => { fetch(API_URL, { headers: { Authorization: API_KEY }, // Never do this! });};✅ Do: Use API routes to protect secrets
// Good: Using API routeconst fetchData = () => { fetch("/api/data"); // API key stays server-side};❌ Don’t: Assume all GraphQL data will be complete
// Bad: Assuming data existsconst { title, content } = data.article;✅ Do: Check for required data
// Good: Validating dataif (!data?.article || !data?.article?.content) { return Astro.rewrite("/404");}Decision Checklist
Section titled “Decision Checklist”When implementing data fetching, ask:
-
Data Requirements
- What is the minimum required data?
- How should partial data be handled?
- What are the fallback options?
-
Fetching Strategy
- Can this be fetched server-side?
- If client-side, is an API route needed?
- Would Astro actions be appropriate?
-
Error Handling
- Are errors logged with context?
- Are appropriate status codes returned?
- Is user feedback provided?
-
Security
- Are secrets protected?
- Is sensitive data handled properly?
- Are requests validated?
Testing Data Fetching
Section titled “Testing Data Fetching”For testing data fetching implementations, refer to our Testing Patterns guide, which covers:
- Mocking fetch requests
- Testing API routes
- Error case testing
- Loading state verification
Accessibility Considerations
Section titled “Accessibility Considerations”When implementing data fetching that affects UI state, follow our Accessibility Requirements for:
- Loading indicators
- Error messages
- Status updates
- Focus management