How to write generic CRUD controller for all entities in golang?

320 Views Asked by At

I am creating a Go server using GoFiber to return data from MySQL database. I am using GORM library to save and fetch data from the db. I have 8 entities in total. I have defined model for these entites like this

package models

type Account struct {
    ID        uint      `json:"id" gorm:"primary_key;auto_increment;not_null"`
    Name      string    `json:"name"`
    Company   string    `json:"company"`
    GSTIN     string    `json:"gstin"`
    AccountNo string    `json:"accountNo" gorm:"unique"`
    IFSC      string    `json:"ifsc"`
    CreatedAt time.Time `json:"createdAt"`
    UpdatedAt time.Time `json:"updatedAt"`
}

Now, for each entity, I am writing 4 controller methods: Create, Update, List, Delete. It is basically the same code for each entity, just that the entity name is changing.

package controllers

// GET: List call
func GetAccounts(c *fiber.Ctx) error {
    accounts := new([]models.Account)
    result := database.DB.Find(accounts)
    if result.Error != nil {
        return result.Error
    }
    c.SendStatus(http.StatusOK)
    return c.JSON(accounts)
}

// POST
func CreateAccount(c *fiber.Ctx) error {
    account := new(models.Account)
    err := c.BodyParser(account)
    if err != nil {
        return err
    }
    result := database.DB.Create(account)
    if result.Error != nil{
        return result.Error
    }
    return c.SendStatus(http.StatusCreated)
}

Now, like this, I have written 8 x 4 =32 controller methods. All having duplicate code with just the entity name changing.

I have defined routes for each of these controllers manually as well.

    app.Post("api/account", controllers.CreateAccount)
    app.Get("api/accounts", controllers.GetAccounts)

There is definitely a better way to do this. I am not exactly sure how to do so or implement it. What interfaces should I define, what structs should I embedd, or what to do?

2

There are 2 best solutions below

0
On

You can make use of generics. See the example below. The T represents the type of the data to be created.

func Create[T any](c *fiber.Ctx, data T) error {
    result := database.DB.Create(data)
    if result.Error != nil {
        return result.Error
    }

    return nil
}
0
On

You can do it like this:

func Get[T any]() fiber.Handler {
    return func(c *fiber.Ctx) error {
        models := new([]T)
        result := database.DB.Find(models)
        if result.Error != nil {
           return result.Error
        }
        c.SendStatus(http.StatusOK)
        return c.JSON(models)
    }
}

func Create[T any]() fiber.Handler {
    return func(c *fiber.Ctx) error {
        model := new(T)
        err := c.BodyParser(model)
        if err != nil {
            return err
        }
        result := database.DB.Create(model)
        if result.Error != nil{
            return result.Error
        }
        return c.SendStatus(http.StatusCreated)
    }
}

And then for your routing, you have to call your handlers this way:

app.Post("api/account", controllers.Create[models.Account]())
app.Get("api/accounts", controllers.Get[models.Account]())

If you wanna know more about generics in Golang, visit here.