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.

text
models:  Time:    model:      - go-graphql/internal/graph/model.Time  Int64:    model:      -
github.com/99designs/gqlgen/graphql.Int64

And in the schema:

graphql
scalar Time
scalar Int64

This 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:

graphql
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:

go
// 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:

graphql
query {  product(id: 1) {    id    name    price  }}query {  products {    id    name    description
price  }}j

The 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.