121 Case Study: Product & Category GraphQL API with gqlgen
title: 121 Case Study: Product & Category GraphQL API with gqlgen
subtitle: Breaking Down the Architecture, Implementation, and Case Study of a Product-Categories API with gqlgen in Golang
author: insinyursoftware
date: 2024-06-22
tags: [graphql, golang, gqlgen, backend, studi-kasus, api]
GraphQL is becoming increasingly popular as a flexible API solution, especially for complex data-relationship scenarios. Among the many tools in the Go ecosystem, gqlgen stands out as one of the most solid frameworks for building GraphQL APIs in Golang.
In this article, I’ll share my experience building a simple API for products and categories, complete with a case study, query simulations, and a discussion of the code architecture using gqlgen. To make things clearer, I’ve included code samples, a simple database schema, and a visualization of the data-fetching flow between entities.
1. Case Study
Let’s imagine we’re building a simple e-commerce system with only two main entities:
- Product
- Has an
id,nama(name),harga(price), andkategori_id.
- Has an
- Category
- Has an
idandnama(name).
- Has an
Relationships between the entities:
- A Product belongs to one Category.
- A Category can have many Products.
We want to provide an API capable of:
- Querying the list of products along with their category details.
- Querying the list of categories along with their products.
- Supporting filtering and relationships.
2. Database Structure
Here is the relational table layout we’ll use:
| produk | kategori |
|---|---|
| id (int) | id (int) |
| nama (string) | nama (string) |
| harga (int) | |
| kategori_id |
3. Designing the GraphQL Schema
We start by defining the GraphQL schema (schema.graphqls):
1type Produk {
2 id: ID!
3 nama: String!
4 harga: Int!
5 kategori: Kategori! # this is the Kategori relation (One-to-Many)
6}
7
8type Kategori {
9 id: ID!
10 nama: String!
11 produk: [Produk!]! # this is the Produk relation (Many-to-One)
12}
13
14type Query {
15 produkList: [Produk!]!
16 kategoriList: [Kategori!]!
17 produkByKategori(kategoriId: ID!): [Produk!]!
18}4. Project Structure
Make sure your project structure looks roughly like this:
1.
2├── gqlgen.yml
3├── go.mod
4├── main.go
5├── graph
6│ ├── model
7│ │ └── models_gen.go
8│ ├── resolver.go
9│ ├── schema.graphqls
10│ ├── schema.resolvers.go
11│ └── database.go5. Implementation with gqlgen
a. Generate the Code Skeleton
First, install it:
1go get github.com/99designs/gqlgen
2go run github.com/99designs/gqlgen generateAfter that, gqlgen will generate the boilerplate for the resolvers and models.
b. Creating a Mock Database
For this case study, a simple in-memory data store is enough.
1// graph/database.go
2
3package graph
4
5import "graph/model"
6
7var kategoriData = []*model.Kategori{
8 {ID: "1", Nama: "Elektronik"},
9 {ID: "2", Nama: "Fashion"},
10}
11
12var produkData = []*model.Produk{
13 {ID: "1", Nama: "Laptop", Harga: 10000000, KategoriID: "1"},
14 {ID: "2", Nama: "HP", Harga: 4000000, KategoriID: "1"},
15 {ID: "3", Nama: "Celana Jeans", Harga: 250000, KategoriID: "2"},
16}Add the KategoriID field to the model. If auto-generation doesn’t support it yet, edit the model directly:
1// graph/model/models_gen.go
2type Produk struct {
3 ID string `json:"id"`
4 Nama string `json:"nama"`
5 Harga int `json:"harga"`
6 KategoriID string `json:"kategoriId"` // important for the relation!
7 Kategori *Kategori `json:"kategori"`
8}c. Implementing the Resolvers
The produkList Query
1// graph/schema.resolvers.go
2
3func (r *queryResolver) ProdukList(ctx context.Context) ([]*model.Produk, error) {
4 return produkData, nil
5}
6
7func (r *queryResolver) KategoriList(ctx context.Context) ([]*model.Kategori, error) {
8 return kategoriData, nil
9}
10
11func (r *queryResolver) ProdukByKategori(ctx context.Context, kategoriID string) ([]*model.Produk, error) {
12 var result []*model.Produk
13 for _, p := range produkData {
14 if p.KategoriID == kategoriID {
15 result = append(result, p)
16 }
17 }
18 return result, nil
19}Relationship Field Resolvers
Because the Produk type in the schema has a kategori field, we need a custom resolver for it:
1func (r *produkResolver) Kategori(ctx context.Context, obj *model.Produk) (*model.Kategori, error) {
2 for _, k := range kategoriData {
3 if k.ID == obj.KategoriID {
4 return k, nil
5 }
6 }
7 return nil, errors.New("kategori not found")
8}And the Kategori type has a produk field (many-to-one):
1func (r *kategoriResolver) Produk(ctx context.Context, obj *model.Kategori) ([]*model.Produk, error) {
2 var result []*model.Produk
3 for _, p := range produkData {
4 if p.KategoriID == obj.ID {
5 result = append(result, p)
6 }
7 }
8 return result, nil
9}6. Query Simulation
Querying Products Along with Their Category
1query {
2 produkList {
3 id
4 nama
5 harga
6 kategori {
7 id
8 nama
9 }
10 }
11}Response:
1{
2 "data": {
3 "produkList": [
4 {
5 "id": "1",
6 "nama": "Laptop",
7 "harga": 10000000,
8 "kategori": {
9 "id": "1",
10 "nama": "Elektronik"
11 }
12 },
13 ...
14 ]
15 }
16}Querying Categories and Their Product Lists
1query {
2 kategoriList {
3 id
4 nama
5 produk {
6 id
7 nama
8 harga
9 }
10 }
11}Response:
1{
2 "data": {
3 "kategoriList": [
4 {
5 "id": "1",
6 "nama": "Elektronik",
7 "produk": [
8 { "id": "1", "nama": "Laptop", "harga": 10000000 },
9 { "id": "2", "nama": "HP", "harga": 4000000 }
10 ]
11 },
12 // and so on
13 ]
14 }
15}7. Product and Category Query Flow Diagram
It’s important to understand the resolver flow and how entities are resolved against one another. Here’s a simple mermaid diagram.
graph TD
A[Client] -- produkList query --> B[Query Resolver]
B -- return array produk --> C[Produk Resolver]
C -- resolve kategori field --> D[Kategori Resolver]
D -- fetch Kategori dari produk.KategoriID --> E[KategoriData]
8. Product-Category Relational Table
| Produk | Kategori |
|---|---|
| Laptop | Elektronik |
| HP | Elektronik |
| Celana Jeans | Fashion |
9. Production Tips
- For real data, swap the storage for a database (PostgreSQL, MySQL, etc.), then abstract the query logic inside the resolvers.
- Use the DataLoader pattern so that field resolvers don’t suffer from N+1 (see the gqlgen dataloader ).
- Error handling and input validation on mutations/queries should definitely be improved.
- Separate the service and repository layers for scalability.
10. Conclusion
This product-category API case is a template you’ll find very frequently in real-world applications. With gqlgen, we can easily build the schema, resolvers, and data-relationship implementation in GraphQL. This short case study has demonstrated clean code structure, simple mock data, and even query simulations and data-flow visualization, all of which can be easily adapted to larger projects.
I hope this explanation opens up new insight into applying GraphQL in Golang for flexible and scalable relational data needs.
References
Bonus — Find the example code repo here (*if repo available)