Beranda Blog Simple CRUD with GoFiber
Development 6 min read

Simple CRUD with GoFiber

Nurul

Nurul

Lead & Fullstack Engineer · December 23, 2023

A practical walkthrough of building a clean REST API in Go using GoFiber, GORM, and a layered architecture — routes, handlers, usecases, and repositories.

Building web APIs with Go is straightforward, and using a framework like GoFiber speeds things up further. This article walks through a simple product CRUD API — covering the project structure, each architectural layer, and how they connect.

The full source code is available at github.com/uhkrowi/go-simple-crud.

Getting Started

Make sure your environment has Go installed. The project uses MySQL for persistence — you can swap it for another database, but you'll need to update the GORM driver and .env config accordingly.

Entry Point

At /cmd/app/main.go, the main() function initialises the app and starts the server. A separate setup() function wires up the database, validator, and routes:

func main() {
    defer config.CloseDBConnection()

    app := fiber.New(fiber.Config{
        AppName: "CRUD",
    })

    app.Use(cors.New())

    setup(app)

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    err := app.Listen(":" + port)
    if err != nil {
        panic(err)
    }
}

func setup(app *fiber.App) {
    config.InitDB()
    db := config.DBConn
    validate := validator.New()

    apiV1 := app.Group("/api/v1")
    route.ProductRoute(apiV1.Group("/product"), db, validate)
}

The Models

The Product model lives at /internal/app/model/product.go. A product has a list of Variant records. The gorm:"-" tag tells GORM to ignore Variants during direct DB operations — it's populated manually in the usecase layer.

type Product struct {
    ID       uuid.UUID  `gorm:"primaryKey;column:id" json:"id"`
    Name     string     `gorm:"column:name" json:"name"`
    IsActive bool       `gorm:"column:is_active" json:"is_active"`
    Variants []Variant  `gorm:"-" json:"variants"`
}

type Variant struct {
    ID          uuid.UUID `gorm:"primaryKey;column:id" json:"id"`
    Name        string    `gorm:"column:name" json:"name"`
    ProductID   uuid.UUID `gorm:"column:product_id" json:"product_id"`
    ProductName string    `gorm:"column:product_name" json:"product_name"`
    Price       float64   `gorm:"column:price" json:"price"`
    Stock       int       `gorm:"column:stock" json:"stock"`
}

Layered Architecture

The project follows a clean layered structure. Each layer has one responsibility:

  • Route — maps HTTP endpoints to the correct handler. For example, GET /api/v1/product calls ProductHandler.GetAll.
  • Handler — receives the HTTP request, parses and validates input, calls the usecase, and returns the response. It owns request/response shaping but no business logic.
  • Usecase — the core logic layer. It orchestrates calls to the repository, applies business rules (e.g., "a product can only be deactivated if it has no active variants"), and may call other usecases.
  • Repository — all database interaction lives here. Usecases call repositories through interfaces, which keeps the business logic independently testable.

Create Flow (Example)

When a client sends POST /api/v1/product:

  1. The router directs the request to ProductHandler.Create.
  2. The handler parses the JSON body into a Product struct and runs validation via go-playground/validator. If validation fails, it returns a 400 with details immediately.
  3. The usecase generates a UUID for the new product, then calls ProductRepository.Create.
  4. The repository runs db.Create(&product) via GORM and returns any error.
  5. The handler receives the result and returns 201 with the created product, or 500 if something went wrong.

Why This Structure?

Separating these layers may feel like extra boilerplate for a small CRUD app, but the payoff comes quickly as the project grows:

  • You can test usecases without a real database by swapping the repository with a mock.
  • Changing the database (say, from MySQL to PostgreSQL) only touches the repository layer.
  • Adding a second transport layer (e.g., gRPC alongside HTTP) reuses the same usecase and repository code.

For a small API it's optional. For anything production-facing, it's worth the setup cost from day one.

Penulis

Nurul

Nurul

Lead & Fullstack Engineer

Bagian dari tim SimpleFunc yang membangun solusi bersih dan skalabel untuk bisnis di seluruh Indonesia.

Kerja Sama

Punya proyek? Mari kita diskusikan bagaimana kami bisa membantu membangunnya.

Hubungi Kami