25 Using Interface and Union Types in graphql-go
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:
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:
1union SearchResult = Human | Droid | StarshipImplementing 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
1go get github.com/graphql-go/graphql2. Defining the Interface
Let’s create the Character interface.
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:
ResolveTypeis 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
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:
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:
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
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:
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:
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.
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
| Aspect | Interface | Union |
|---|---|---|
| Required fields | Must have certain fields (defined by the interface) | Not required to share the same fields |
| Primary use case | Polymorphism with a uniform set of fields | Polymorphism without a uniform set of fields |
| Example scenario | User: Admin, Guest, Member | A search result that can be various object types |
9. Testing the Endpoint with Go
You can test the endpoint with the following code:
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! 🚀