Skip to Content

GraphQL

Gofasta includes GraphQL support powered by gqlgen , a type-safe Go GraphQL server library. When enabled, your project ships with a working GraphQL endpoint alongside the REST API, both sharing the same services and repositories.

Prerequisite: GraphQL support is opt-in. You must create your project with the --graphql flag to include GraphQL files, the gqlgen dependency, and the /graphql endpoint:

gofasta new myapp --graphql

If you created a REST-only project (without --graphql), the app/graphql/ directory, gqlgen.yml, and GraphQL routes will not be present.

How It Works

GraphQL in Gofasta follows this flow:

Schema (.gql files) → gqlgen generates → Resolver stubs → You implement → Service layer
  1. You define your schema in .gql files under app/graphql/schema/
  2. Run go generate ./... (or gofasta g resolver) to generate resolver stubs
  3. Implement the resolver methods by calling your existing services
  4. Both REST and GraphQL share the same service and repository layers

Project Structure

app/graphql/ ├── schema/ │ ├── common.gql # Shared scalars/inputs (pagination, sorting) │ ├── server.health.gql # Root Query/Mutation + health check │ ├── user.gql # User type and operations │ └── product.gql # Added per resource by `g scaffold --graphql` └── resolvers/ ├── resolver.go # Resolver struct with service dependencies ├── user.resolvers.go # User resolver implementations (gqlgen-owned) ├── gql_errors.go # Shared error helpers (not gqlgen-owned) └── gql_filters.go # GraphQL input → domain filter conversion app/generated.go # Auto-generated runtime code (do not edit) app/dtos/generated-types.dtos.go # Auto-generated Go types from schema # (app/shared/dtos/ in feature layout)

The gqlgen.yml file at the project root controls code generation paths and type mappings.

Defining a Schema

Schema files use standard GraphQL SDL syntax. Each resource typically gets its own .gql file.

# app/graphql/schema/product.gql type Product { id: ID! name: String! price: Float! createdAt: DateTime! updatedAt: DateTime! } input CreateProductInput { name: String! price: Float! } input UpdateProductInput { name: String price: Float } extend type Query { products(page: Int, perPage: Int): ProductConnection! product(id: ID!): Product! } extend type Mutation { createProduct(input: CreateProductInput!): Product! updateProduct(id: ID!, input: UpdateProductInput!): Product! deleteProduct(id: ID!): Boolean! }

The root schema file defines the base types and any custom scalars:

# app/graphql/schema/schema.gql scalar DateTime type Query type Mutation type Subscription type PageInfo { page: Int! perPage: Int! total: Int! totalPages: Int! } type ProductConnection { nodes: [Product!]! pageInfo: PageInfo! }

Generating Resolvers

After modifying schema files, regenerate the resolver stubs:

make gqlgen # runs `go tool gqlgen generate`

To wire an existing service into the resolver root, use the Gofasta CLI:

gofasta g resolver <Resource>

gqlgen updates the {name}.resolvers.go file matching each schema file with new stub methods for any schema additions. Existing implementations are preserved — only new stubs are added.

Implementing Resolvers

The resolver struct holds references to your services, injected via Wire:

// app/graphql/resolvers/resolver.go (layered layout) package resolvers import svcInterfaces "myapp/app/services/interfaces" type Resolver struct { UserService svcInterfaces.UserServiceInterface ProductService svcInterfaces.ProductServiceInterface }

In feature layout the same struct references the feature packages instead: UserService userpkg.UserServiceInterface with userpkg "myapp/app/user" — see GraphQL and project layouts.

Resolver methods call the same services used by REST controllers:

// app/graphql/resolvers/product.resolvers.go (layered layout) package resolvers import ( "context" "errors" "gorm.io/gorm" "myapp/app/dtos" "myapp/app/services" ) // CreateProduct is the resolver for the createProduct field. func (r *mutationResolver) CreateProduct(ctx context.Context, input dtos.TCreateProductDto) (*dtos.Product, error) { if errs := r.Validator.ValidateStruct(input); len(errs) > 0 { return nil, validationGqlError(errs) } product, err := r.ProductService.Create(ctx, input.ToCreateInput()) if err != nil { return nil, internalGqlError("failed to create product", err) } return dtos.ProductFromModel(product), nil } // FindProductByID is the resolver for the findProductById field. func (r *queryResolver) FindProductByID(ctx context.Context, filters dtos.TFindProductByIDDto) (*dtos.Product, error) { if errs := r.Validator.ValidateStruct(filters); len(errs) > 0 { return nil, validationGqlError(errs) } product, err := r.ProductService.Get(ctx, filters.ProductID) switch { case errors.Is(err, services.ErrProductNotFound), errors.Is(err, gorm.ErrRecordNotFound): return nil, gqlError("NOT_FOUND", "product not found") case err != nil: return nil, internalGqlError("failed to find product", err) } return dtos.ProductFromModel(product), nil }

(In feature layout the per-resource references read productpkg.TCreateProductDto / productpkg.ErrProductNotFound instead — gofasta refactor rewrites them for you.) The gqlError / validationGqlError / internalGqlError helpers live in gql_errors.go.

Subscriptions

gqlgen supports WebSocket-based subscriptions for real-time data:

# app/graphql/schema/product.gql extend type Subscription { productCreated: Product! }

Implement the subscription resolver with a channel:

func (r *subscriptionResolver) ProductCreated(ctx context.Context) (<-chan *dtos.Product, error) { ch := make(chan *dtos.Product, 1) go func() { defer close(ch) // Listen for events from your event system for { select { case <-ctx.Done(): return case product := <-r.ProductService.OnCreated(): ch <- product } } }() return ch, nil }

GraphQL Playground

In development, the GraphQL Playground is available at:

http://localhost:8080/graphql-playground

The GraphQL endpoint itself is at:

http://localhost:8080/graphql

You can use the playground to explore your schema, run queries, and test mutations interactively.

Authentication in GraphQL

GraphQL requests pass through the same middleware stack as REST. The JWT auth middleware extracts the user from the Authorization header and sets claims on the request context, which is accessible in resolvers:

func (r *mutationResolver) CreateProduct(ctx context.Context, input dtos.TCreateProductDto) (*dtos.Product, error) { claims, err := auth.ClaimsFromContext(ctx) if err != nil { return nil, err } subjectID := claims.SubjectID() // the `sub` claim role := claims.Role // Use subjectID and role for authorization logic // ... }

gqlgen Configuration

The gqlgen.yml file at your project root controls code generation. The scaffold ships it in this shape (layered layout):

schema: - app/graphql/schema/*.gql exec: filename: app/generated.go package: app model: filename: app/dtos/generated-types.dtos.go package: dtos resolver: layout: follow-schema dir: app/graphql/resolvers package: resolvers filename_template: "{name}.resolvers.go" autobind: - "myapp/app/dtos"

The autobind setting tells gqlgen to map GraphQL types to your existing DTO structs when names match, avoiding duplicate type definitions. Anything the schema declares that has no matching DTO is generated into the model.filename file.

GraphQL and project layouts

Gofasta supports two project layouts (layered and feature-package); the GraphQL surface adapts to both:

  • File locations don’t change. app/graphql/schema/ and app/graphql/resolvers/ are shared directories in both layouts — gqlgen owns the resolver directory, so resolvers are never split across feature packages.
  • What the resolvers reference changes. In layered, resolvers import <mod>/app/dtos and <mod>/app/services; in feature layout the per-resource symbols live in app/<r>/, so resolvers use the feature alias (userpkg.TCreateUserDto, userpkg.ErrUserNotFound) and only shared/generated types keep the dtos. qualifier (imported from <mod>/app/shared/dtos).
  • gqlgen.yml follows the layout. Feature-layout projects autobind against <mod>/app/shared/dtos plus one <mod>/app/<r> entry per resource, and generate models into app/shared/dtos/generated-types.dtos.go.
  • gofasta refactor handles all of it. Switching layouts rewrites the resolver files and gqlgen.yml and re-runs go tool gqlgen generate — see refactor → GraphQL projects. Use --all on GraphQL projects.
  • gofasta g scaffold <R> --graphql works in both layouts: on a feature project it wires the resolver with the feature alias and appends the new <mod>/app/<r> autobind entry automatically.

Adding a New GraphQL Resource

  1. Create a schema file in app/graphql/schema/order.gql
  2. Define the type, inputs, and extend Query/Mutation
  3. Run go generate ./... to generate resolver stubs
  4. Implement the resolver methods by calling your services
  5. Add the service dependency to the Resolver struct

If you generated the resource with gofasta g scaffold, the schema file and resolver are already created for you.

Next Steps

Last updated on