GraphQL Query Patterns
This guide shows you how to implement GraphQL operations, including type generation, query organization, data transformations, and best practices.
For component-level data fetching patterns, see Data Fetching Patterns.
Type Architecture
Section titled “Type Architecture”Our GraphQL implementation uses a structured three-layer approach for type safety and maintainability:
- Generated Types - Auto-generated from Contentful GraphQL schema
- Transform Types - Custom interfaces for transformed data structures
- Transform Functions - Convert raw GraphQL data to UI-ready formats
Type Generation with GraphQL Codegen
Section titled “Type Generation with GraphQL Codegen”GraphQL Codegen automatically generates TypeScript types from the Contentful GraphQL schema:
# Generate types from schema and queriesnpm run codegenConfiguration: src/graphql/codegen.ts
- Connects to Contentful GraphQL API using environment variables
- Scans for
.cms.graphqlquery files - Generates types in
src/graphql/generated/contentful/schema.ts
Query Files: Use .cms.graphql extension for Contentful queries processed by codegen:
query Homepage { internalPageCollection(where: { slug: "homepage" }) { items { title hero { # ... query fields } } }}Transform Types Pattern
Section titled “Transform Types Pattern”Create custom interfaces representing the final shape of data consumed by UI components:
export interface TransformedData { title: string; hero: HeroData; contentBlocks: ContentBlockItem[]; adTargeting: AdTargeting; metatags?: Metadata[];}Type Import Patterns
Section titled “Type Import Patterns”Generated Types:
import type { HomepageQuery } from "#graphql/generated/contentful/schema";Transform Types:
import type { TransformedData } from "./_graphql/transform.types";Extracting Specific Types from Generated Queries:
// Extract nested types from generated query responsesexport type PromotionFromQuery = NonNullable< NonNullable< NonNullable<HomepageQuery["internalPageCollection"]>["items"][0] >["hero"]>["introduction"]["promotion"];
// Extract union type variantsexport type ShopPromotion = Extract<PromotionFromQuery, { __typename: "Shop" }>;export type InternalPagePromotion = Extract< PromotionFromQuery, { __typename: "InternalPage" }>;Transform Function Pattern
Section titled “Transform Function Pattern”Transform functions convert raw GraphQL responses to UI-ready data:
import type { HomepageQuery } from "#graphql/generated/contentful/schema";import type { TransformedData } from "./transform.types";
export function transformHomepageData(data: HomepageQuery): TransformedData { const page = data.internalPageCollection?.items[0];
return { title: page?.title ?? "", hero: transformHero(page?.hero), contentBlocks: transformContentBlocks(page?.componentsCollection), adTargeting: { url: "/homepage" }, };}Query Organization
Section titled “Query Organization”Directory Structure
Section titled “Directory Structure”We organize GraphQL operations following a feature-based structure. For more details on component organization, see Component Architecture.
src/ graphql/ fragments/ # Shared fragments imageProps.fragment.graphql articleListItem.fragment.graphql pages/ articles/[slug]/ _graphql/ # Page-specific queries page.query.graphql # Main query (shell and above-fold) validate.query.graphql # Validation query api.ts # API functions transform.ts # Data transformations _components/ # Page components relatedArticles/ # Leaf node component query.graphql # Component-specific query api.ts # Component API transform.ts # Component transforms🤔 Why This Pattern?
- Clear separation of concerns
- Reusable fragments
- Colocated queries with components
- Support for HTML streaming
- Progressive loading
Leaf Node Architecture
Section titled “Leaf Node Architecture”We follow a leaf node architecture pattern where components beyond the page shell and above-the-fold content fetch their own data. This pattern works in conjunction with our Component Architecture and Data Fetching Patterns.
---import { getArticleBySlug } from "./_graphql/api";import RelatedArticles from "./_components/relatedArticles/RelatedArticles.astro";
// Shell and above-fold dataconst article = await getArticleBySlug(Astro.params.slug);
if (!article) { return Astro.rewrite("/404");}---
<Layout title={article.title}> {/* Above-fold content */} <ArticleHero data={article.hero} /> <ArticleContent content={article.content} />
{/* Leaf node components with colocated queries */} <RelatedArticles /> {/* No need to pass slug - available via Astro.params */} <PopularArticles /> {/* Can access URL params directly */}</Layout>---import { getRelatedArticles } from "./api";
// Component has access to the same request contextconst { slug } = Astro.params;const articles = await getRelatedArticles(slug);---
<section> {articles.map(article => <ArticleCard article={article} />)}</section>🤔 Why This Pattern?
- Supports HTML streaming or server islands (astro) using the server:defer attribute
- Shared request context across components
- Reduced prop drilling
- Clear data boundaries
- Better performance
- Simpler maintenance
Component Query Organization
Section titled “Component Query Organization”Leaf node components maintain their own GraphQL files and can access request data directly:
import { graphqlApi } from "#graphql/graphqlClient";import RELATED_ARTICLES_QUERY from "./query.graphql";import { transformArticles } from "./transform";
export async function getRelatedArticles(slug: string) { const { data } = await graphqlApi({ query: RELATED_ARTICLES_QUERY, variables: { slug } });
return transformArticles(data.relatedArticles);}
// Usage in component:---const { slug } = Astro.params; // Access URL params directlyconst { url } = Astro; // Access full URLconst { cookies } = Astro; // Access cookiesconst articles = await getRelatedArticles(slug);---🤔 Why This Pattern?
- Colocated queries with components
- Direct access to request context
- Simplified component props
- Clear data boundaries
- Easier testing
Best Practices for Leaf Nodes
Section titled “Best Practices for Leaf Nodes”-
Data Independence
- Each leaf component manages its own data
- Queries are colocated with components
- Direct access to request context
- Clear transformation boundaries
- Independent error handling
-
Performance
- Leverage HTML streaming
- Use server:defer for lower priorityasync content
- Progressive component loading
- Efficient data fetching
- Appropriate caching
-
Organization
- Feature-based directory structure
- Access request data directly when needed
- Clear component boundaries
- Maintainable transforms
-
Props vs Context
- Use request context for URL/params/cookies
- Pass explicit props for component-specific data
- Avoid unnecessary prop drilling
- Keep components self-contained
Query Naming
Section titled “Query Naming”We follow consistent naming patterns for GraphQL files:
# ✅ Do: Descriptive, namespaced queries (these will show up in logs and error messages)query Homepage { landingPage: page(slug: "homepage") { title meta { metaTags } }}
# ❌ Don't: Generic or ambiguous namesquery GetData { # wrong page { title }}Fragment Composition
Section titled “Fragment Composition”Reusable Fragments
Section titled “Reusable Fragments”We use fragments to share common field selections:
#import "#graphql/fragments/imageProps.fragment.graphql"
fragment ArticleListItem on Article { title esid date featuredImage { ...ImageProperties } readTime authors { firstName lastName slug image { ...ImageProperties } } slug excerpt tags { slug title } meta { type { slug title } coreContentTags { slug title } }}
# Usage in page queryquery Articles { articles(page: { size: 15 }) { items { ...ArticleListItem } }}🤔 Why This Pattern?
- DRY field selections
- Consistent data shapes
- Type safety
- Easy maintenance
Fragment Imports
Section titled “Fragment Imports”Use explicit imports for fragments:
# ✅ Do: Explicit imports#import "#graphql/fragments/imageProps.fragment.graphql"
# ❌ Don't: Relative paths#import "../../../fragments/imageProps.fragment.graphql" # wrongData Transformations
Section titled “Data Transformations”Transform Pattern
Section titled “Transform Pattern”Transform GraphQL responses into component-friendly shapes:
import type { ArticleQuery } from "./types";
interface TransformedArticle { title: string; content: string; meta: { tags: string[]; readTime: number; };}
export function transformArticleData(data: ArticleQuery): TransformedArticle { return { title: data.article.title, content: data.article.content, meta: { tags: data.article.tags.map(tag => tag.title), readTime: data.article.readTime, }, };}🤔 Why This Pattern?
- Clean data shapes
- Type safety
- Separation of concerns
- Consistent transformations
API Integration
Section titled “API Integration”API Functions
Section titled “API Functions”Encapsulate GraphQL operations in typed functions:
import { graphqlApi } from "#graphql/graphqlClient";import ARTICLE_QUERY from "./page.query.graphql";import type { ArticleQuery, ArticleQueryVariables } from "./types";import { transformArticleData } from "./transform";
export async function getArticleBySlug(slug: string) { try { const { data } = await graphqlApi<ArticleQuery, ArticleQueryVariables>({ query: ARTICLE_QUERY, variables: { slug }, });
// Validate minimum required data if (!data?.article?.content) { console.error("Missing required article data", { component: "getArticleBySlug", slug, }); return null; }
return transformArticleData(data); } catch (err) { console.error("Failed to fetch article", { error: err.message, component: "getArticleBySlug", slug, }); return null; }}🤔 Why This Pattern?
- Type-safe operations
- Consistent error handling
- Data validation
- Clean API surface
Best Practices
Section titled “Best Practices”-
Query Organization
- Colocate queries with features
- Use fragments for shared fields
- Follow naming conventions
- Keep queries focused
-
Type Safety
- Generate types from schemas
- Use TypeScript interfaces
- Validate responses
- Transform consistently
-
Error Handling
- Validate required fields
- Log structured errors
- Return clean nulls
- Include context
-
Performance
- Request only needed fields
- Use pagination
- Cache appropriately
- Monitor query times
Anti-Patterns to Avoid
Section titled “Anti-Patterns to Avoid”❌ Don’t: Inline fragments
# Bad: Repeated field selectionsquery Articles { articles { items { title date # Duplicated fields } }}✅ Do: Use shared fragments
# Good: Reusable fragmentsquery Articles { articles { items { ...ArticleListItem } }}❌ Don’t: Skip data validation
// Bad: Assuming data existsconst { title, content } = data.article;✅ Do: Validate required fields
// Good: Checking required fieldsif (!data?.article?.content) { return null;}Decision Checklist
Section titled “Decision Checklist”When implementing GraphQL operations, ask:
-
Query Structure
- Is it colocated with feature?
- Are fragments used appropriately?
- Is the naming consistent?
- Are types generated?
-
Data Requirements
- Are fields minimal?
- Is pagination needed?
- Are transforms defined?
- Is caching considered?
-
Error Handling
- Are required fields validated?
- Is error logging in place?
- Are nulls handled?
- Is context included?
-
Performance
- Are queries optimized?
- Is caching configured?
- Are transforms efficient?
- Is monitoring in place?
Testing GraphQL Operations
Section titled “Testing GraphQL Operations”For testing GraphQL operations, refer to our Testing Patterns guide, which covers:
- Unit testing transforms
- Integration testing queries
- Mocking GraphQL responses
- Testing error cases
Accessibility Considerations
Section titled “Accessibility Considerations”When implementing GraphQL operations that affect UI state, ensure you follow our Accessibility Requirements for:
- Loading states
- Error messages
- Status updates
- Focus management