Building a GraphQL API in Go with Custom Scalars
GraphQL has become a powerful alternative to REST APIs, offering clients the ability to request exactly the data they need. In Go, the gqlgen library makes it straightforward to build type‑safe GraphQL servers that integrate cleanly with existing service layers.
In this article, we’ll walk through how to expose a Product type via GraphQL, handle custom scalars like Time and Int64, and wire resolvers to your service layer.
Go-GraphQL Project
Why GraphQL?
Unlike REST, GraphQL allows clients to:
- Fetch multiple resources in a single request.
- Specify exactly which fields they want.
- Handle evolving schemas without breaking existing clients.
This flexibility is especially useful in e‑commerce or product catalog systems, where clients may need different slices of product data depending on context.
Defining Custom Scalars
By default, GraphQL only supports a limited set of scalar types (Int, String, Boolean, etc.). In Go, we often need richer types like time.Time or int64. To handle this, we declare custom scalars in our schema and configure them in gqlgen.yml.
models: Time: model: - go-graphql/internal/graph/model.Time Int64: model: -
github.com/99designs/gqlgen/graphql.Int64And in the schema:
scalar Time
scalar Int64This tells gqlgen to map GraphQL Time to our Go model.Time type, and Int64 to the provided implementation in graphql.Int64. Without this mapping, gqlgen would default to int for Int, which is only 32‑bit and unsuitable for large values like product prices.
Schema for Products
We define our product types and queries in schema.graphqls:
type Product {
id: Int!
name: String!
description: String!
price: Int64!
}
type ProductListResponse {
id: Int!
name: String!
description: String!
price: Int64!
}
type ErrorResponse {
code: String!
message: String!
}
type Query {
"""
Get a single product by ID
"""
product(id: Int!): Product
"""
Get a list of all products
"""
products: [Product!]!
}Here, price is explicitly declared as Int64! to ensure we can handle large numeric values safely.
Implementing Resolvers
Resolvers connect GraphQL queries to your service layer. For example:
// Product is the resolver for the product field.
func (r *queryResolver) Product(ctx context.Context, id int) (*model.Product, error) {
product, err := r.ProductService.GetProductByID(ctx, int32(id))
if err != nil {
return nil, fmt.Errorf("failed to get product: %w", err)
}
return &model.Product{
ID: int(product.ID), Name: product.Name, Description: product.Description, Price:
product.Price,
}, nil
}
// Products is the resolver for the products field.
func (r *queryResolver) Products(ctx context.Context) ([]*model.Product, error) {
products, err := r.ProductService.ListProducts(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list products: %w", err)
}
var result []*model.Product
for _, p := range products {
result = append(result, &model.Product{
ID: int(p.ID), Name: p.Name, Description: p.Description, Price: p.Price,
})
}
return result, nil
}These resolvers delegate to the existing ProductService, ensuring that both REST and GraphQL layers share the same business logic.
Example Queries
Clients can now query your API like this:
query { product(id: 1) { id name price }}query { products { id name description
price }}jThe server responds with exactly the requested fields, no more and no less.
Git Commit
Conclusion
By introducing custom scalars (Time, Int64) and wiring resolvers to your service layer, you’ve created a robust GraphQL API in Go. This approach ensures type safety, handles large numeric values correctly, and provides clients with flexible queries over your product catalogue.
GraphQL in Go with gqlgen is not only powerful but also developer‑friendly, letting you evolve your API without breaking existing clients. With these changes, your system is ready to support modern, efficient data access patterns.
