Skip to main content

Fragments: GraphQL's Copy-Paste on Steroids

· 7 min read
GraphQL Guy

GraphQL Fragments

You're copying the same fields across 17 different queries. Your code reviewer is crying. Your future self is plotting revenge. Enter fragments: the DRY principle applied to GraphQL queries.

The Copy-Paste Nightmare

Here's code I've seen in production (okay, code I wrote):

# UserProfile.tsx
query GetUserProfile($id: ID!) {
user(id: $id) {
id
firstName
lastName
email
avatar
bio
createdAt
isVerified
followersCount
followingCount
}
}

# UserCard.tsx
query GetUserForCard($id: ID!) {
user(id: $id) {
id
firstName
lastName
email
avatar
bio
createdAt
isVerified
followersCount
followingCount
}
}

# UserSettings.tsx
query GetUserSettings($id: ID!) {
user(id: $id) {
id
firstName
lastName
email
avatar
bio
createdAt
isVerified
followersCount
followingCount
# Plus a few more fields
notificationPreferences {
emailEnabled
pushEnabled
}
}
}

Three queries. Same 10 fields copy-pasted. When avatar became avatarUrl in the schema, I had to update 17 files.

Fragments to the Rescue

A fragment is a reusable piece of a query:

fragment UserCore on User {
id
firstName
lastName
email
avatar
bio
createdAt
isVerified
followersCount
followingCount
}

Now those queries become:

# UserProfile.tsx
query GetUserProfile($id: ID!) {
user(id: $id) {
...UserCore
}
}

# UserCard.tsx
query GetUserForCard($id: ID!) {
user(id: $id) {
...UserCore
}
}

# UserSettings.tsx
query GetUserSettings($id: ID!) {
user(id: $id) {
...UserCore
notificationPreferences {
emailEnabled
pushEnabled
}
}
}

One change to UserCore, all 17 files updated. My future self sent me a thank-you note.

Fragment Anatomy 101

fragment FragmentName on TypeName {
field1
field2
nestedObject {
nestedField
}
}
PartPurpose
fragmentKeyword
FragmentNameYour chosen name (PascalCase convention)
on TypeNameThe GraphQL type this fragment applies to
{ ... }Fields to include

The Fragment Taxonomy

Not all fragments are created equal. Here's my taxonomy:

Species 1: The Core Fragment

Essential fields that define a type. Used everywhere.

fragment ProductCore on Product {
id
name
slug
price {
amount
currency
}
thumbnail {
url
alt
}
}

Use when: Every query that touches this type needs these fields.

Species 2: The Display Fragment

Fields needed to render a specific UI component.

fragment ProductCard on Product {
...ProductCore
rating
reviewCount
isOnSale
salePrice {
amount
currency
}
}

fragment ProductListItem on Product {
...ProductCore
# Just the basics for a list
}

fragment ProductDetail on Product {
...ProductCore
description
specifications {
name
value
}
images {
url
alt
}
reviews(first: 5) {
...ReviewPreview
}
}

Use when: Different components need different levels of detail.

Species 3: The Nested Fragment

Fragments that use other fragments. Composition FTW.

fragment OrderSummary on Order {
id
status
total {
amount
currency
}
items {
quantity
product {
...ProductCore # Nested fragment
}
}
shippingAddress {
...AddressFields # Another nested fragment
}
}

Use when: Building complex queries from smaller, reusable pieces.

Species 4: The Interface Fragment

Fragments on interfaces for shared fields across types.

fragment NodeFields on Node {
id
}

fragment TimestampFields on Timestamped {
createdAt
updatedAt
}

type Product implements Node & Timestamped {
# ...
}

query {
product(id: "123") {
...NodeFields
...TimestampFields
name
price
}
}

Use when: Multiple types share common fields via interfaces.

Species 5: The Union Fragment

Handling polymorphic types with inline fragments.

fragment SearchResult on SearchResultItem {
... on Product {
...ProductCard
}
... on Article {
...ArticlePreview
}
... on User {
...UserCard
}
}

query Search($query: String!) {
search(query: $query) {
...SearchResult
}
}

Use when: Query returns union types or interfaces with multiple implementations.

Fragment Organization

File Structure

src/
└── graphql/
├── fragments/
│ ├── user.fragments.ts
│ ├── product.fragments.ts
│ ├── order.fragments.ts
│ └── common.fragments.ts
├── queries/
│ ├── user.queries.ts
│ └── product.queries.ts
└── mutations/
└── cart.mutations.ts

Fragment File Example

// src/graphql/fragments/user.fragments.ts
import { gql } from '@apollo/client';

export const USER_CORE_FIELDS = gql`
fragment UserCore on User {
id
firstName
lastName
email
avatar
}
`;

export const USER_PROFILE_FIELDS = gql`
${USER_CORE_FIELDS}

fragment UserProfile on User {
...UserCore
bio
website
location
isVerified
followersCount
followingCount
}
`;

export const USER_SETTINGS_FIELDS = gql`
${USER_CORE_FIELDS}

fragment UserSettings on User {
...UserCore
notificationPreferences {
emailEnabled
pushEnabled
marketingEnabled
}
privacySettings {
profileVisible
showEmail
}
}
`;

Using Fragments in Queries

// src/graphql/queries/user.queries.ts
import { gql } from '@apollo/client';
import { USER_PROFILE_FIELDS } from '../fragments/user.fragments';

export const GET_USER_PROFILE = gql`
${USER_PROFILE_FIELDS}

query GetUserProfile($id: ID!) {
user(id: $id) {
...UserProfile
}
}
`;

The Colocated Fragment Pattern

Popular in modern React apps: keep fragments with components.

// UserAvatar.tsx
import { gql } from '@apollo/client';

export const USER_AVATAR_FRAGMENT = gql`
fragment UserAvatar_user on User {
id
firstName
avatar
}
`;

interface UserAvatarProps {
user: UserAvatar_userFragment; // Generated type
}

export function UserAvatar({ user }: UserAvatarProps) {
return (
<img
src={user.avatar}
alt={user.firstName}
className="avatar"
/>
);
}
// UserCard.tsx
import { gql } from '@apollo/client';
import { USER_AVATAR_FRAGMENT, UserAvatar } from './UserAvatar';

export const USER_CARD_FRAGMENT = gql`
${USER_AVATAR_FRAGMENT}

fragment UserCard_user on User {
...UserAvatar_user
firstName
lastName
bio
}
`;

export function UserCard({ user }: { user: UserCard_userFragment }) {
return (
<div className="user-card">
<UserAvatar user={user} />
<h3>{user.firstName} {user.lastName}</h3>
<p>{user.bio}</p>
</div>
);
}
// UserProfilePage.tsx
import { gql, useQuery } from '@apollo/client';
import { USER_CARD_FRAGMENT, UserCard } from './UserCard';

const GET_USER = gql`
${USER_CARD_FRAGMENT}

query GetUser($id: ID!) {
user(id: $id) {
...UserCard_user
}
}
`;

export function UserProfilePage({ userId }: { userId: string }) {
const { data } = useQuery(GET_USER, { variables: { id: userId } });

return data?.user ? <UserCard user={data.user} /> : null;
}

Benefits:

  • Components declare their own data requirements
  • TypeScript types are automatically scoped
  • Refactoring is contained
  • Easy to see what data a component needs

Fragment Masking (The Advanced Technique)

Fragment masking ensures components can only access data they declared:

// With Relay or urql's graphcache
import { useFragment } from 'react-relay';
import { UserCard_user$key } from './__generated__/UserCard_user.graphql';

function UserCard({ userRef }: { userRef: UserCard_user$key }) {
const user = useFragment(
graphql`
fragment UserCard_user on User {
firstName
lastName
avatar
}
`,
userRef
);

// TypeScript only allows access to firstName, lastName, avatar
// Even if the parent query fetched more fields
return (
<div>
<img src={user.avatar} />
<span>{user.firstName}</span>
</div>
);
}

This prevents components from accidentally depending on data they didn't declare.

Performance Implications

Fragment Caching

GraphQL clients normalize data by type + id. Fragments help:

fragment ProductCore on Product {
id # Required for normalization
name
price
}

Query 1 fetches products for a list. Query 2 fetches a single product. If they use the same fragment, the cache recognizes them as the same entity.

┌─────────────────────────────────────────────────────────────┐
│ APOLLO CACHE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Product:123 ─────────────┐ │
│ ├── name: "Widget" │ Both queries reference │
│ ├── price: 29.99 │ the same cached entity │
│ └── ... │ │
│ ▲ ▲ │ │
│ │ │ │ │
│ ProductList ProductDetail │
│ (uses ProductCore) (uses ProductCore) │
│ │
└─────────────────────────────────────────────────────────────┘

Over-fetching with Fragments

Fragments can cause over-fetching if you're not careful:

# Big fragment
fragment ProductFull on Product {
id
name
description # Not needed in list
specifications # Not needed in list
images # Not needed in list
reviews { ... } # REALLY not needed in list
}

# Used in list context - fetches too much
query ProductList {
products(first: 50) {
...ProductFull # Overkill!
}
}

Solution: Create context-specific fragments:

fragment ProductListItem on Product {
id
name
price
thumbnail
}

fragment ProductDetailPage on Product {
...ProductListItem
description
specifications
images
reviews { ... }
}

Fragment Anti-Patterns

Anti-Pattern 1: The God Fragment

# DON'T DO THIS
fragment Everything on User {
id
firstName
lastName
email
# ... 50 more fields
orders { ... }
reviews { ... }
# ... every relation
}

Problem: Every query fetches everything. Defeats the purpose of GraphQL.

Anti-Pattern 2: Deeply Nested Fragments

fragment A on User {
...B
}

fragment B on User {
...C
}

fragment C on User {
...D
}

# 10 levels later...
fragment J on User {
id
name
}

Problem: Hard to understand what's actually being fetched. Debugging nightmare.

Anti-Pattern 3: Circular Fragment Dependencies

fragment UserWithPosts on User {
...PostAuthor # References User
posts {
...PostWithAuthor
}
}

fragment PostWithAuthor on Post {
author {
...UserWithPosts # Circular!
}
}

Problem: Infinite loops. GraphQL will reject this, but the error is confusing.

Testing with Fragments

// Fragment extraction for testing
import { getFragmentDefinitions } from '@apollo/client/utilities';

describe('UserCard fragment', () => {
it('should include required fields', () => {
const definitions = getFragmentDefinitions(USER_CARD_FRAGMENT);
const fields = definitions[0].selectionSet.selections
.map((s: any) => s.name.value);

expect(fields).toContain('id');
expect(fields).toContain('firstName');
expect(fields).toContain('avatar');
});
});

Mock Data for Fragments

import { MockedProvider } from '@apollo/client/testing';

const mockUser: UserCard_userFragment = {
__typename: 'User',
id: '123',
firstName: 'Jane',
lastName: 'Doe',
avatar: 'https://example.com/avatar.jpg',
};

test('UserCard renders correctly', () => {
render(<UserCard user={mockUser} />);
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
});

Fragment Best Practices Cheat Sheet

┌────────────────────────────────────────────────────────────────────┐
│ FRAGMENT BEST PRACTICES │
├────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ DO ❌ DON'T │
│ ───────────────────────────── ───────────────────────────── │
│ Always include `id` Create "God fragments" │
│ Use context-specific fragments Nest more than 3 levels deep │
│ Colocate with components Share across unrelated features │
│ Name descriptively Use generic names like "Fields" │
│ Compose with smaller fragments Duplicate fields across fragments │
│ Version fragments carefully Change without checking usage │
│ │
└────────────────────────────────────────────────────────────────────┘

Conclusion

Fragments transform GraphQL queries from copy-paste chaos to composable, maintainable code. They're the difference between "update this field in 47 places" and "update this fragment once."

Start with core fragments for your main types. Create display fragments for specific components. Compose them together for complex queries. Your future self will thank you.

And if your code reviewer stops crying, you'll know you did it right.


No developers were harmed in the refactoring of 47 queries into 5 fragments.