Skip to content

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.

Use API routes to protect sensitive data and proxy requests:

routes/api/search.ts
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,
});
}
}

For form handling patterns, see our detailed Forms and User Input Patterns guide.

For detailed GraphQL patterns, including leaf node architecture and fragment composition, see GraphQL Query Patterns.

Use GraphQL in Astro pages for server-side data fetching:

pages/index.astro
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

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 query

Example API implementation:

pages/articles/[slug]/_graphql/api.ts
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
  1. Action Definition

    • Define schemas for validation
    • Provide meaningful error messages
    • Handle all error cases
    • Return consistent response shapes
  2. Form Implementation

    • Use HTML forms for simple cases
    • Use SolidJS for complex forms
    • Show loading states
    • Provide clear feedback
  3. Error Handling

    • Validate input server-side
    • Return structured errors
    • Show user-friendly messages
    • Log errors appropriately
  4. Security

    • Validate all inputs
    • Sanitize data
    • Rate limit submissions
    • Prevent CSRF attacks

Don’t: Skip server-side validation

// Bad: Client-only validation
const handler = ({ email }) => {
// No validation before use
return subscribeUser(email);
};

Do: Always validate server-side

// Good: Server-side validation
const 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 implementation
fetch("/api/subscribe", {
method: "POST",
body: JSON.stringify({ email }),
});

Do: Use Astro actions

// Good: Type-safe action calls
const result = await actions.newsletter({
email: email(),
});

When implementing forms, ask:

  1. Form Complexity

    • Is JavaScript required?
    • Are there complex interactions?
    • Is real-time validation needed?
  2. Data Handling

    • Is input validation defined?
    • Are all error cases handled?
    • Is feedback provided?
  3. User Experience

    • Are loading states shown?
    • Is error feedback clear?
    • Is success feedback shown?
  4. Security

    • Is data validated?
    • Are inputs sanitized?
    • Is rate limiting implemented?

GraphQL allows partial data returns. Handle these cases gracefully:

pages/destination/[slug].astro
---
import { destinationAPI } from "./_graphql/api";
const data = await destinationAPI(Astro.params.slug);
if (!data) {
return Astro.rewrite("/404");
}
// Transform data for components
const { 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
  1. 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
  2. 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
  3. Error Handling

    • Log structured errors with context
    • Return appropriate status codes (404/500)
    • Provide user-friendly error messages
    • Include error recovery options
  4. Data Validation

    • Define minimum content requirements
    • Check for required fields
    • Handle partial data gracefully
    • Log missing data appropriately

Don’t: Expose API keys in client-side code

// Bad: Exposing API key
const fetchData = () => {
fetch(API_URL, {
headers: { Authorization: API_KEY }, // Never do this!
});
};

Do: Use API routes to protect secrets

// Good: Using API route
const fetchData = () => {
fetch("/api/data"); // API key stays server-side
};

Don’t: Assume all GraphQL data will be complete

// Bad: Assuming data exists
const { title, content } = data.article;

Do: Check for required data

// Good: Validating data
if (!data?.article || !data?.article?.content) {
return Astro.rewrite("/404");
}

When implementing data fetching, ask:

  1. Data Requirements

    • What is the minimum required data?
    • How should partial data be handled?
    • What are the fallback options?
  2. Fetching Strategy

    • Can this be fetched server-side?
    • If client-side, is an API route needed?
    • Would Astro actions be appropriate?
  3. Error Handling

    • Are errors logged with context?
    • Are appropriate status codes returned?
    • Is user feedback provided?
  4. Security

    • Are secrets protected?
    • Is sensitive data handled properly?
    • Are requests validated?

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

When implementing data fetching that affects UI state, follow our Accessibility Requirements for:

  • Loading indicators
  • Error messages
  • Status updates
  • Focus management