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
--graphqlflag to include GraphQL files, the gqlgen dependency, and the/graphqlendpoint:gofasta new myapp --graphqlIf you created a REST-only project (without
--graphql), theapp/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- You define your schema in
.gqlfiles underapp/graphql/schema/ - Run
go generate ./...(orgofasta g resolver) to generate resolver stubs - Implement the resolver methods by calling your existing services
- 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-playgroundThe GraphQL endpoint itself is at:
http://localhost:8080/graphqlYou 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/andapp/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/dtosand<mod>/app/services; in feature layout the per-resource symbols live inapp/<r>/, so resolvers use the feature alias (userpkg.TCreateUserDto,userpkg.ErrUserNotFound) and only shared/generated types keep thedtos.qualifier (imported from<mod>/app/shared/dtos). gqlgen.ymlfollows the layout. Feature-layout projects autobind against<mod>/app/shared/dtosplus one<mod>/app/<r>entry per resource, and generate models intoapp/shared/dtos/generated-types.dtos.go.gofasta refactorhandles all of it. Switching layouts rewrites the resolver files andgqlgen.ymland re-runsgo tool gqlgen generate— see refactor → GraphQL projects. Use--allon GraphQL projects.gofasta g scaffold <R> --graphqlworks 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
- Create a schema file in
app/graphql/schema/order.gql - Define the type, inputs, and extend Query/Mutation
- Run
go generate ./...to generate resolver stubs - Implement the resolver methods by calling your services
- Add the service dependency to the
Resolverstruct
If you generated the resource with gofasta g scaffold, the schema file and resolver are already created for you.