GraphQL Generates

Overview

When you run go run github.com/99designs/gqlgen generate, gqlgen reads your GraphQL schema and generates type-safe Go code for your GraphQL server. This document explains exactly what gets generated and why.

Generated Files

gqlgen generates 3 main files:
FileLinesPurpose
generated.go~4000Complete GraphQL server engine
models_gen.go~50Input/output types from schema
*.resolvers.go~100Resolver stubs (you implement)

1. generated.go - The GraphQL Server Engine

Size: ~4,093 lines
Purpose: Complete GraphQL server implementation
You modify: ❌ Never (auto-generated)

What It Contains

A. Resolver Interfaces

gqlgen generates type-safe interfaces from your schema:
// From your GraphQL schema:
// type Query {
//   user(id: ID!): User
// }

// gqlgen generates:
type QueryResolver interface {
    User(ctx context.Context, id string) (*domain.User, error)
}
Key interfaces:
type MutationResolver interface {
    CreateUser(ctx context.Context, input CreateUserInput) (*domain.User, error)
    UpdateUser(ctx context.Context, id string, input UpdateUserInput) (*domain.User, error)
}

type QueryResolver interface {
    User(ctx context.Context, id string) (*domain.User, error)
    Users(ctx context.Context, limit *int, offset *int) (*UserList, error)
    SearchUsers(ctx context.Context, email string) ([]*domain.User, error)
}

type UserResolver interface {
    DisplayName(ctx context.Context, obj *domain.User) (string, error)
    CreatedAt(ctx context.Context, obj *domain.User) (string, error)
}

B. Argument Parsing

Converts GraphQL arguments to Go types:
// Parses: user(id: "123")
func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
    var err error
    args := map[string]any{}
    arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", ec.unmarshalNID2string)
    if err != nil {
        return nil, err
    }
    args["id"] = arg0
    return args, nil
}

C. Field Resolution

Handles each GraphQL field:
// Resolves: { user { email } }
func (ec *executionContext) _Query_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
    return graphql.ResolveField(
        ctx,
        ec.OperationContext,
        field,
        ec.fieldContext_Query_user,
        func(ctx context.Context) (any, error) {
            fc := graphql.GetFieldContext(ctx)
            return ec.resolvers.Query().User(ctx, fc.Args["id"].(string))
        },
        nil,
        ec.marshalOUser2ᚖgithubᚗcomᚋwaynecheahᚋgoᚑcoreᚋinternalᚋmodulesᚋuserᚋdomainᚐUser,
        true,
        false,
    )
}

D. Marshaling/Unmarshaling

Converts between Go types and GraphQL JSON:
// Marshals domain.User to GraphQL JSON
func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋwaynecheahᚋgoᚑcoreᚋinternalᚋmodulesᚋuserᚋdomainᚐUser(ctx context.Context, sel ast.SelectionSet, v *domain.User) graphql.Marshaler {
    // ... complex marshaling logic
}

E. Complexity Calculation

For query cost analysis:
type ComplexityRoot struct {
    Query struct {
        User        func(childComplexity int, id string) int
        Users       func(childComplexity int, limit *int, offset *int) int
    }
    User struct {
        Email       func(childComplexity int) int
        DisplayName func(childComplexity int) int
    }
}

F. Introspection Support

Powers GraphQL Playground:
func (ec *executionContext) introspectSchema() (*introspection.Schema, error) {
    if ec.DisableIntrospection {
        return nil, errors.New("introspection disabled")
    }
    return introspection.WrapSchema(ec.Schema()), nil
}

2. models_gen.go - Input/Output Types

Size: ~36 lines
Purpose: Types that don’t exist in your domain
You modify: ❌ Never (auto-generated)

What It Contains

A. Input Types

From your GraphQL schema inputs:
# GraphQL Schema
input CreateUserInput {
  email: String!
  name: String!
  password: String!
  roles: [String!]!
}
// Generated Go struct
type CreateUserInput struct {
    Email    string   `json:"email"`
    Name     string   `json:"name"`
    Password string   `json:"password"`
    Roles    []string `json:"roles"`
}

B. Optional Fields (Pointers)

For update operations:
# GraphQL Schema
input UpdateUserInput {
  email: String # Optional
  name: String # Optional
  roles: [String!] # Optional
}
// Generated with pointers for optional fields
type UpdateUserInput struct {
    Email *string  `json:"email,omitempty"`   // nil = not provided
    Name  *string  `json:"name,omitempty"`    // nil = not provided
    Roles []string `json:"roles,omitempty"`
}

C. Custom Output Types

Types that don’t exist in domain:
# GraphQL Schema
type UserList {
  users: [User!]!
  total: Int!
  hasMore: Boolean!
}
// Generated output type
type UserList struct {
    Users   []*domain.User `json:"users"`    // Uses existing domain.User!
    Total   int            `json:"total"`
    HasMore bool           `json:"hasMore"`
}
Notice: It reuses your domain.User instead of generating a new type!

3. *.resolvers.go - Resolver Stubs

Size: ~100 lines
Purpose: Implementation stubs for you to fill in
You modify: ✅ Yes! This is where you write code

What It Contains

A. Generated Stubs

gqlgen creates stub implementations:
// Generated stub (panics by default)
func (r *queryResolver) User(ctx context.Context, id string) (*domain.User, error) {
    panic(fmt.Errorf("not implemented: User - user"))
}

B. Your Implementation

You replace the panic with actual logic:
// Your implementation
func (r *queryResolver) User(ctx context.Context, id string) (*domain.User, error) {
    // Call your existing service!
    return r.userService.Get(ctx, id)
}

C. Resolver Types

gqlgen generates resolver type definitions:
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type userResolver struct{ *Resolver }

// These implement the interfaces from generated.go
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
func (r *Resolver) User() UserResolver { return &userResolver{r} }

How It All Works Together

1. You Write GraphQL Schema

# schema/user.graphql
type User {
  id: ID!
  email: String!
  displayName: String! # Computed field
}

type Query {
  user(id: ID!): User
}

2. Run Code Generation

go run github.com/99designs/gqlgen generate

3. gqlgen Generates Code

generated.go (4093 lines)
├─ QueryResolver interface
├─ Argument parsing
├─ Field resolution
├─ Marshaling
└─ Introspection

models_gen.go (36 lines)
├─ CreateUserInput
├─ UpdateUserInput
└─ UserList

user.resolvers.go (100 lines)
├─ Stub: User()
├─ Stub: CreateUser()
└─ Stub: DisplayName()

4. You Implement Resolvers

// user.resolvers.go
func (r *queryResolver) User(ctx context.Context, id string) (*domain.User, error) {
    return r.userService.Get(ctx, id)  // Your code!
}

func (r *userResolver) DisplayName(ctx context.Context, obj *domain.User) (string, error) {
    return fmt.Sprintf("%s <%s>", obj.Name, obj.Email), nil  // Your code!
}

5. Wire Up in main.go

gqlResolver := gqlHandler.NewResolver(userService, userQueryService, logger)
gqlServer := handler.NewDefaultServer(gqlHandler.NewExecutableSchema(gqlHandler.Config{
    Resolvers: gqlResolver,
}))

e.POST("/graphql", echo.WrapHandler(gqlServer))

6. GraphQL API Ready!

query {
  user(id: "123") {
    email
    displayName # Calls your resolver!
  }
}

Key Benefits

1. Type Safety

// ✅ Compiler catches errors
type QueryResolver interface {
    User(ctx context.Context, id string) (*domain.User, error)
}

// ❌ This won't compile (wrong signature)
func (r *queryResolver) User(ctx context.Context, id int) (*domain.User, error) {
    // Compiler error: id should be string
}

2. No Manual JSON Parsing

// ❌ Without gqlgen (manual)
func HandleGraphQL(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Query string `json:"query"`
    }
    json.NewDecoder(r.Body).Decode(&req)
    // Parse query string
    // Extract arguments
    // Call handler
    // Marshal response
    // ... 500+ lines
}

// ✅ With gqlgen (generated)
func (r *queryResolver) User(ctx context.Context, id string) (*domain.User, error) {
    return r.userService.Get(ctx, id)  // That's it!
}

3. Automatic Updates

When you change your schema:
# Add a new field to schema
type User {
  id: ID!
  email: String!
  phoneNumber: String!  # New field
}

# Regenerate
go run github.com/99designs/gqlgen generate

# gqlgen adds stub
func (r *userResolver) PhoneNumber(ctx context.Context, obj *domain.User) (string, error) {
    panic(fmt.Errorf("not implemented: PhoneNumber"))
}

# You implement it
func (r *userResolver) PhoneNumber(ctx context.Context, obj *domain.User) (string, error) {
    return obj.Phone, nil
}

Comparison: Manual vs gqlgen

Without gqlgen

// You write ALL of this manually:

// 1. Parse GraphQL query string
// 2. Validate syntax
// 3. Extract operation (query/mutation)
// 4. Extract fields
// 5. Extract arguments
// 6. Type checking
// 7. Call resolvers
// 8. Marshal response
// 9. Handle errors
// 10. Introspection support

// Result: 5000+ lines of boilerplate

With gqlgen

// gqlgen generates 4000+ lines for you
// You just write business logic:

func (r *queryResolver) User(ctx context.Context, id string) (*domain.User, error) {
    return r.userService.Get(ctx, id)
}

// Result: 10 lines of code

File Size Breakdown

internal/modules/user/adapter/inbound/graphql/
├── generated.go          4,093 lines  (auto-generated)
├── models_gen.go            36 lines  (auto-generated)
├── user.resolvers.go       137 lines  (you implement)
├── resolver.go              27 lines  (you write)
└── schema/
    └── user.graphql         60 lines  (you write)

Total generated: 4,129 lines
Total you write:   224 lines

Ratio: gqlgen writes 95% of the code!

Summary

gqlgen generates:
  1. generated.go (4,093 lines)
    • Complete GraphQL server
    • Type-safe interfaces
    • Argument parsing
    • Marshaling/unmarshaling
    • Introspection
  2. models_gen.go (36 lines)
    • Input types (CreateUserInput, UpdateUserInput)
    • Output types (UserList)
    • Reuses domain types when possible
  3. *.resolvers.go (137 lines)
    • Stub implementations
    • You fill in the business logic
    • Type-safe method signatures
You write:
  • GraphQL schema (.graphql files)
  • Resolver implementations (fill stubs)
  • Wire up in main.go
Result: Production-ready, type-safe GraphQL API with minimal code!

Why This Matters

Without gqlgen:
  • 5000+ lines of boilerplate
  • Manual type checking
  • Error-prone JSON parsing
  • Hard to maintain
With gqlgen:
  • 200 lines of business logic
  • Compiler-enforced type safety
  • Auto-generated parsing
  • Easy to maintain
This is why gqlgen is the most popular GraphQL library for Go! 🚀