Mastering AppSync: Unions and Interfaces

Published: (February 12, 2026 at 08:36 AM EST)
6 min read
Source: Dev.to

Source: Dev.to

Introducing AppSync to Your Team

Introducing AppSync to your team has its challenges—especially if the team is unfamiliar with GraphQL (GQL). As the AppSync APPSYNC_JS resolver expert, you’ll be expected to explain:

  • Merged APIs
  • How to test changes
  • Fundamentals of GQL types, schemas, and queries

In my experience, most teams pick up GQL basics quickly, but they often struggle with Union and Interface types.

When should they choose one over the other? What makes them different? Does the team need them at all?

Do We Need Interfaces or Unions at All?

Short answer: No, you don’t have to use them.

However, interfaces and unions let you express schema constraints that are otherwise hard to capture. Below is an example that illustrates why they can be useful.

Example: An Error Type

Imagine an error type that informs the user that their input is invalid. In its basic form it has the following properties:

FieldTypeDescription
codeString!Machine‑readable error code
messageString!Human‑readable error message

In GQL this can be expressed as:

type ValidationError {
  code: String!
  message: String!
}

Extending the Basic Error

Specific error variants may add extra fields. Consider two concrete errors:

  • MissingPropertiesError – tells which required fields are missing
  • ImproperAgeError – tells the minimum allowed age

Both share code and message, but each adds its own optional field:

type MissingPropertiesError {
  code: String!
  message: String!
  missingProperties: [String!]   # e.g. ["firstName", "email"]
}

type ImproperAgeError {
  code: String!
  message: String!
  minimumAge: Int                # e.g. 18
}

The client can inspect the error type and read the extra property for more detail.

What the Simple Schema Can’t Express

The schema above does not convey that missingProperties and minimumAge are mutually exclusive—you’ll receive one error type or the other, never both.

Other API description languages (e.g., OpenAPI) have a oneOf construct for this. In GraphQL you achieve the same exclusivity with interfaces or unions.

GraphQL Interfaces

What Is an Interface?

  • In OO languages, an interface is an abstraction of a class, defining methods and properties that implementing classes must support.
  • In GraphQL, an interface is even simpler: it defines a set of fields that any implementing type must expose. No methods, only fields.

Defining an Interface for Errors

interface ValidationError {
  code: String!
  message: String!
}

Now the concrete error types implement this interface:

type MissingPropertiesError implements ValidationError {
  code: String!
  message: String!
  missingProperties: [String!]!
}

type ImproperAgeError implements ValidationError {
  code: String!
  message: String!
  minimumAge: Int!
}

Using the Interface in a Mutation

type Mutation {
  saveInput(input: Input!): ValidationError
}

Querying with Inline Fragments

mutation Save($input: Input!) {
  saveInput(input: $input) {
    __typename               # tells you which concrete type you got
    ... on ValidationError {
      code
      message
    }
    ... on MissingPropertiesError {
      missingProperties
    }
    ... on ImproperAgeError {
      minimumAge
    }
  }
}

The __typename field is automatically added by GraphQL and lets the client discriminate between the possible concrete types.
The fragments make the either‑or nature of minimumAge vs. missingProperties explicit.

GraphQL Unions

What Is a Union?

  • A union groups separate, unrelated types.
  • Unlike an interface, a union does not define any common fields.

When Might You Use a Union?

  • When the result set can be heterogeneous (e.g., full‑text search returning articles, users, and tags).
  • Generally, returning unrelated types from a single field is poor API design—think of a REST endpoint that could return shoes, ships, or sealing wax.

Defining a Union for Errors

type MissingPropertiesError {
  code: String!
  message: String!
  missingProperties: [String!]!
}

type ImproperAgeError {
  code: String!
  message: String!
  minimumAge: Int!
}

union ValidationError = MissingPropertiesError | ImproperAgeError

Using the Union in a Mutation

type Mutation {
  saveInput(input: Input!): ValidationError
}

Querying a Union

Because a union has no common fields, you must request everything inside fragments:

mutation Save($input: Input!) {
  saveInput(input: $input) {
    __typename
    ... on MissingPropertiesError {
      code
      message
      missingProperties
    }
    ... on ImproperAgeError {
      code
      message
      minimumAge
    }
  }
}

You still get mutual exclusivity, but you lose the convenience of shared fields.

Choosing Between Interface and Union

Decision FactorInterfaceUnion
Common fields exist?✅ Use an interface❌ Not suitable
Types are conceptually related?✅ Use an interface❌ Not ideal
Result set is heterogeneous (e.g., search results)?❌ Prefer a union✅ Use a union
Single field returns multiple, unrelated types?✅ Use a union (rare)

Rule of thumb:
If there is any shared field, prefer an interface. If the types are truly unrelated, a union may be appropriate.

GraphQL “Code Smells” for Interfaces & Unions

  1. Optional mutually exclusive fields – Good candidate for an interface (e.g., the overloaded Error type shown earlier).
  2. Sharing only trivial fields (e.g., just id) – Avoid creating an interface; the cognitive overhead outweighs the benefit.
  3. Interfaces on full‑text search results – Suspicious; unions usually shine here because search can return many unrelated types.
  4. Unions on non‑search endpoints – Usually a smell; look for a common interface instead, unless you’re forced by an upstream API.

Recap

  • Interfaces = shared fields + concrete implementations → use when there’s commonality.
  • Unions = completely separate types → use for heterogeneous results (e.g., search).
  • Both expose __typename so the client can discriminate at runtime.
  • Avoid over‑engineering: only add interfaces/unions when they solve a real problem (mutual exclusivity, shared fields, heterogeneous results).

With this understanding, you’ll be able to guide your team through the nuances of GraphQL’s type system while introducing AppSync’s powerful resolver model. Happy schema designing!

AWS AppSync GraphQL Developer Guide: Interfaces and Unions in GraphQL

Schemas and Types

They will look to you for answers and advice. Having examples and a few rules of thumb to guide decision‑making can go a long way toward helping your team succeed.

Happy building!

0 views
Back to Blog

Related posts

Read more »

shadcn & ai give me superpower....

While working on the frontend of my project, I used shadcn/ui, and it has been a great experience. The components are clean, stable, and highly customizable. Si...