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 via ($name: Type!) syntax
Composition...FragmentName spread syntax${fragment} interpolation or .spread() method
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 with tagged template syntax to define a fragment:

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

export const userFragment = gql.default(({ fragment }) =>
  fragment("UserFragment", "User")`{
    id
    name
    email
  }`(),
);

The tagged template specifies the GraphQL type and field selections directly. soda-gql validates the fragment against your schema at build time.

For advanced features like field aliases, you can use the tagged template with alias syntax:

export const userFragment = gql.default(({ fragment }) =>
  fragment("UserFragment", "User")`{
    userId: id
    name
    email
  }`(),
);

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. In tagged template syntax, declare variables in the template:

export const userFragment = gql.default(({ fragment }) =>
  fragment("UserFragment", "Query")`($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
    }
  }`(),
);

Alternatively, the same fragment with variables in tagged template syntax:

export const userFragment = gql.default(({ fragment }) =>
  fragment("UserFragment", "Query")`($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
    }
  }`(),
);

Spreading Fragments

In tagged templates, use ${...} interpolation to spread fragments:

export const postFragment = gql.default(({ fragment }) =>
  fragment("PostFragment", "Post")`{
    id
    title
    author {
      ...${userFragment}
    }
  }`(),
);

For fragments with variables, use a callback function to pass variable bindings:

export const getPostQuery = gql.default(({ query }) =>
  query("GetPost")`($postId: ID!, $showEmail: Boolean) {
    post(id: $postId) {
      id
      title
      author {
        ...${({ $ }) => userFragment.spread({ includeEmail: $.showEmail })}
      }
    }
  }`(),
);

In the options-object path, use .spread() directly:

export const getPostQuery = gql.default(({ query }) =>
  query("GetPost")({
    variables: `($postId: ID!, $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("UserFragment", "User")`{
      id
      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. With tagged template syntax, the first argument to fragment() serves as the key:

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

The first argument to fragment() serves as the key in tagged template syntax:

export const userFragment = gql.default(({ fragment }) =>
  fragment("UserFields", "User")`{
    id
    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