Fragments

Fragments are reusable, type-safe field selections for GraphQL types. They define how to select and structure fields from your schema.

How soda-gql Fragments Differ from GraphQL

In standard GraphQL, fragments are defined as strings within your query documents:

fragment UserFields on User {
  id
  name
  email
}

soda-gql takes a different approach:

AspectGraphQLsoda-gql
DefinitionString-based, inside .graphql filesTypeScript functions with full IDE support
Type SafetyRequires external codegenBuilt-in type inference
VariablesNot supported in standard GraphQL fragmentsFirst-class support with $var
Composition...FragmentName spread syntax.spread() method with typed variable passing
IDE SupportLimited (depends on tooling)Full autocomplete, go-to-definition, refactoring
TIP

soda-gql fragments with variables are similar to Relay's @argumentDefinitions directive, but work without any GraphQL server extensions.

Defining a Fragment

Use the gql.default() pattern to define a fragment:

import { gql } from "@/graphql-system";

export const userFragment = gql.default(({ fragment }) =>
  fragment.User({
    fields: ({ f }) => ({
      ...f.id(),
      ...f.name(),
      ...f.email(),
    }),
  }),
);

The fragment.User call specifies the GraphQL type this fragment applies to. The field builder (f) provides type-safe access to all fields defined on that type.

Field Selection API

Basic Fields

Select scalar fields directly:

...f.id()      // Select the id field
...f.name()    // Select the name field

Fields with Arguments

Pass arguments as an object:

f.posts({ limit: 10, offset: 0 })
f.avatar({ size: "LARGE" })

Nested Selections

For fields that return object types, use curried syntax to select nested fields:

...f.posts({ limit: 10 })(({ f }) => ({
  ...f.id(),
  ...f.title(),
  ...f.author()(({ f }) => ({
    ...f.id(),
    ...f.name(),
  })),
}))

Field Aliases

Rename fields in the response using the alias option:

...f.id(null, { alias: "userId" })
...f.name(null, { alias: "displayName" })

Fragment Variables

Unlike standard GraphQL fragments, soda-gql fragments can declare their own variables:

export const userFragment = gql.default(({ fragment, $var }) =>
  fragment.User({
    variables: { ...$var("userId").ID("!") },
    fields: ({ f, $ }) => ({
      ...f.user({ id: $.userId })(({ f }) => ({
        ...f.id(),
        ...f.name(),
        ...f.email(),
      })),
    }),
  }),
);

Variables are declared using object spread syntax with $var(). The variable reference ($) provides typed access to these variables within field arguments.

Spreading Fragments

Spread fragments in other fragments or operations using .spread():

export const postFragment = gql.default(({ fragment }) =>
  fragment.Post({
    fields: ({ f }) => ({
      ...f.id(),
      ...f.title(),
      ...f.author()(({ f }) => ({
        ...userFragment.spread({ includeEmail: false }),
      })),
    }),
  }),
);

When spreading a fragment with variables, pass the values through the first argument:

// Parent operation with its own variable
export const getPostQuery = gql.default(({ query, $var }) =>
  query.operation({
    name: "GetPost",
    variables: {
      ...$var("postId").ID("!"),
      ...$var("showEmail").Boolean("?"),
    },
    fields: ({ f, $ }) => ({
      ...f.post({ id: $.postId })(({ f }) => ({
        ...f.id(),
        ...f.title(),
        ...f.author()(({ f }) => ({
          // Pass parent variable to spread fragment
          ...userFragment.spread({ includeEmail: $.showEmail }),
        })),
      })),
    }),
  }),
);

Type Inference

Extract TypeScript types from fragments using $infer:

// Input type (variables required to use this fragment)
type UserInput = typeof userFragment.$infer.input;
// { includeEmail?: boolean }

// Output type (shape of selected fields)
type UserOutput = typeof userFragment.$infer.output;
// { id: string; name: string; email?: string }

This is useful for typing function parameters or component props:

function UserCard({ user }: { user: typeof userFragment.$infer.output }) {
  return <div>{user.name}</div>;
}

Extending Fragments with attach()

The attach() method allows adding custom properties to fragments. This is useful for colocating related functionality:

import type { GqlElementAttachment } from "@soda-gql/core";

export const userFragment = gql
  .default(({ fragment }) =>
    fragment.User({
      fields: ({ f }) => ({
        ...f.id(),
        ...f.name(),
      }),
    }),
  )
  .attach({
    name: "displayName",
    createValue: (element) => (user: typeof element.$infer.output) =>
      user.name.toUpperCase(),
  });

// Use the attached function
const formatted = userFragment.displayName(userData);

For colocation patterns, see the Fragment Colocation guide.

Fragment Keys

The optional key property gives a fragment a unique identifier:

export const userFragment = gql.default(({ fragment }) =>
  fragment.User({
    key: "UserFields",
    fields: ({ f }) => ({
      ...f.id(),
      ...f.name(),
    }),
  }),
);

When Keys Are Required

Fragments work correctly without keys at runtime. However, keys become important when using prebuilt types:

ScenarioKey Required?
Runtime fragment executionNo
Type inference with $inferNo
Prebuilt type generationYes
Prebuilt Types Requirement

When generating prebuilt types with bun run soda-gql typegen, fragments without keys are silently skipped. If you need a fragment's types in the prebuilt registry, add a key property.

Operations do not need a separate key property - they use their name automatically.

For more details on prebuilt types, see the Prebuilt Types Guide.

Next Steps