Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
25 Jul 2025 · 6 min read ·Article 25 / 125
Go

25 Using Interface and Union Types in graphql-go

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

25 Using Interface and Union Types in graphql-go

GraphQL offers tremendous flexibility when defining an API schema, especially when dealing with data that can take many shapes (polymorphic data). Among these powerful features, Interface and Union Types are two essential tools for building scalable and maintainable applications. In this article, we will take a comprehensive look at how to use Interface and Union Types in graphql-go, a popular library for building GraphQL servers in Go.


What Are Interface and Union Types in GraphQL?

Before diving into graphql-go, let’s first understand the underlying concepts in GraphQL.

Interface Type

An interface is similar to the interface concept in object-oriented programming. An interface defines a set of fields that must be implemented by any object type that implements it. A query against an interface will return one of the objects that implement it.

A simple example:

graphql
 1interface Character {
 2  id: ID!
 3  name: String!
 4}
 5type Human implements Character {
 6  id: ID!
 7  name: String!
 8  homePlanet: String
 9}
10type Droid implements Character {
11  id: ID!
12  name: String!
13  primaryFunction: String
14}

Union Type

A union defines a type that can be any one of several specified types, without requiring them to share the same fields.

Example:

graphql
1union SearchResult = Human | Droid | Starship

Implementing Interface and Union in graphql-go

For production use, we’ll use the package github.com/graphql-go/graphql .

Scenario: We’re building a Star Wars character API that has two kinds of characters (Human and Droid) along with a search feature (search) whose result can be a Human, Droid, or Starship object.


1. Installation

bash
1go get github.com/graphql-go/graphql

2. Defining the Interface

Let’s create the Character interface.

go
 1var characterInterface = graphql.NewInterface(graphql.InterfaceConfig{
 2    Name: "Character",
 3    Fields: graphql.Fields{
 4        "id": &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
 5        "name": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
 6    },
 7    ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object {
 8        switch p.Value.(type) {
 9        case Human:
10            return humanType
11        case Droid:
12            return droidType
13        default:
14            return nil
15        }
16    },
17})

Explanation:

  • ResolveType is an important function for runtime type resolution. It determines which object should be returned when a client queries an interface.

3. Defining Object Types That Implement the Interface

go
 1type Human struct {
 2    ID         string `json:"id"`
 3    Name       string `json:"name"`
 4    HomePlanet string `json:"homePlanet"`
 5}
 6
 7var humanType = graphql.NewObject(graphql.ObjectConfig{
 8    Name:       "Human",
 9    Interfaces: []*graphql.Interface{characterInterface},
10    Fields: graphql.Fields{
11        "id":         &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
12        "name":       &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
13        "homePlanet": &graphql.Field{Type: graphql.String},
14    },
15})
16
17type Droid struct {
18    ID              string `json:"id"`
19    Name            string `json:"name"`
20    PrimaryFunction string `json:"primaryFunction"`
21}
22
23var droidType = graphql.NewObject(graphql.ObjectConfig{
24    Name:       "Droid",
25    Interfaces: []*graphql.Interface{characterInterface},
26    Fields: graphql.Fields{
27        "id":              &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
28        "name":            &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
29        "primaryFunction": &graphql.Field{Type: graphql.String},
30    },
31})

4. Defining the Union Type

Suppose we also have a Starship type:

go
 1type Starship struct {
 2    ID    string `json:"id"`
 3    Name  string `json:"name"`
 4    Model string `json:"model"`
 5}
 6
 7var starshipType = graphql.NewObject(graphql.ObjectConfig{
 8    Name: "Starship",
 9    Fields: graphql.Fields{
10        "id":    &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
11        "name":  &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
12        "model": &graphql.Field{Type: graphql.String},
13    },
14})
15
16var searchResultUnion = graphql.NewUnion(graphql.UnionConfig{
17    Name:  "SearchResult",
18    Types: []*graphql.Object{humanType, droidType, starshipType},
19    ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object {
20        switch p.Value.(type) {
21        case Human:
22            return humanType
23        case Droid:
24            return droidType
25        case Starship:
26            return starshipType
27        default:
28            return nil
29        }
30    },
31})

5. The GraphQL Schema and Query

Let’s put together a root query that uses both the Interface and the Union:

go
 1var rootQuery = graphql.NewObject(graphql.ObjectConfig{
 2    Name: "Query",
 3    Fields: graphql.Fields{
 4        "character": &graphql.Field{
 5            Type: characterInterface,
 6            Args: graphql.FieldConfigArgument{
 7                "id": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.ID)},
 8            },
 9            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
10                id := p.Args["id"].(string)
11                if h, ok := humanData[id]; ok {
12                    return h, nil
13                }
14                if d, ok := droidData[id]; ok {
15                    return d, nil
16                }
17                return nil, nil
18            },
19        },
20        "search": &graphql.Field{
21            Type: graphql.NewList(searchResultUnion),
22            Args: graphql.FieldConfigArgument{
23                "text": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
24            },
25            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
26                text := p.Args["text"].(string)
27                // Simulating a search
28                results := []interface{}{}
29                for _, h := range humanData {
30                    if strings.Contains(h.Name, text) {
31                        results = append(results, h)
32                    }
33                }
34                for _, d := range droidData {
35                    if strings.Contains(d.Name, text) {
36                        results = append(results, d)
37                    }
38                }
39                for _, s := range starshipData {
40                    if strings.Contains(s.Name, text) {
41                        results = append(results, s)
42                    }
43                }
44                return results, nil
45            },
46        },
47    },
48})

6. Sample Dummy Data

go
 1var humanData = map[string]Human{
 2    "1000": {"1000", "Luke Skywalker", "Tatooine"},
 3}
 4
 5var droidData = map[string]Droid{
 6    "2000": {"2000", "C-3PO", "Protocol"},
 7}
 8
 9var starshipData = map[string]Starship{
10    "3000": {"3000", "Millennium Falcon", "YT-1300"},
11}

Example Queries

A query against the interface:

graphql
 1{
 2  character(id: "1000") {
 3    id
 4    name
 5    ... on Human {
 6      homePlanet
 7    }
 8    ... on Droid {
 9      primaryFunction
10    }
11  }
12}

A query against the union:

graphql
 1{
 2  search(text: "Sky") {
 3    ... on Human {
 4      id
 5      name
 6      homePlanet
 7    }
 8    ... on Droid {
 9      id
10      name
11      primaryFunction
12    }
13    ... on Starship {
14      id
15      name
16      model
17    }
18  }
19}

7. Execution Flow with Interface and Union

Let’s visualize this with a flow diagram using Mermaid.

MERMAID
flowchart TD
    Q1([Client Query])
    S1([Server])
    RT1{Apakah Interface?}
    RT2{Apakah Union?}
    TYPE1([Resolve Type])
    RESP([Response])

    Q1 --> S1
    S1 --> RT1
    RT1 -- Ya --> TYPE1
    RT1 -- Tidak --> RT2
    RT2 -- Ya --> TYPE1
    RT2 -- Tidak --> RESP
    TYPE1 --> RESP

8. Interface vs Union Comparison Table

AspectInterfaceUnion
Required fieldsMust have certain fields (defined by the interface)Not required to share the same fields
Primary use casePolymorphism with a uniform set of fieldsPolymorphism without a uniform set of fields
Example scenarioUser: Admin, Guest, MemberA search result that can be various object types

9. Testing the Endpoint with Go

You can test the endpoint with the following code:

go
 1schema, _ := graphql.NewSchema(graphql.SchemaConfig{
 2    Query: rootQuery,
 3})
 4
 5params := graphql.Params{
 6    Schema:        schema,
 7    RequestString: `{ search(text: "Sky") { ... on Human { id name homePlanet } } }`,
 8}
 9
10result := graphql.Do(params)
11fmt.Printf("%v", result.Data)

The output will be JSON corresponding to the type that was returned.


Conclusion

Using Interface and Union Types in graphql-go makes our API more flexible and ready to handle complex business requirements. With this approach, your Go backend application will be more maintainable, scalable, and easier to integrate with heterogeneous clients.

If you’re building a startup or a large project with Go, be sure to take full advantage of these GraphQL schema features. Combine them with solid testing, monitoring, and documentation so that the developer experience stays great across your entire team.

Happy experimenting and building a solid GraphQL API with graphql-go! 🚀

Related Articles

💬 Comments