Gorm, Fiber, PostgreSQL ve Docker kullanarak Golang REST API
Tags: go, docker, postgres, fiber
Published: 2023-10-07 11:06:19

Bu makalede Go ile basit bir REST API oluşturacağız, burada ki basitten kastım temel CRUD işlemleri içeriyor olmasından kaynaklı yani çok kompleks bir uygulama değil fakat alışılagelmişin dışında bir yol ile ilerliyeceğiz.

Çoğu eğitim videolarında veya okuduğunuz makalelerde klasik olan `main.go` içerisinde örneği açıkla ve bitir olarak devam eder. Biz öyle yapmayacağız, biz bir tasarım deseni yöntemi belirleyeceğiz ve ona uygun bir kodlama yapacağız. Uygulamamız da ayriyeten Dependency Injection (DI) de kullanacağız.

Hadi o zaman kodlamaya başlayalım...

## Bağımlılıklar

1. [Fiber](https://docs.gofiber.io/) (Web Framework & Web Server)
2. [PostgreSQL](https://gorm.io/docs/connecting_to_the_database.html#PostgreSQL) (Veritabanı)
3. [GORM](https://gorm.io/docs/) (ORM)
4. [Docker](https://www.docker.com/) (Konteynerizasyon için)
5. Docker Compose (Konteynerizasyonu yönetmek için)

---

## Giriş

İlk yapacağımız şey uygulamamızın **"folder structure"** yani klasör yapısını oluşturmak olacak. Temelde CRUD işlemleri yapacak olsakda uygulamamızın anlaşılır, temiz ve düzenli yazmak için aslında diğer programlama dillerinden de alışkın olduğumuz bir yöntem bu: **"Controller-Service-Repository" (CSR) tasarım deseni**.

Bu desen, uygulamanın farklı katmanlarını düzenli ve daha bakımı kolay bir şekilde ayırmaya yardımcı olur.

![folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cgxviwek8f1qw7x2f9lb.png)

> **Not :** Bu sadece örnek bir yapıdır, isterseniz kendi proje yapınızı oluşturabilirsiniz! Hatta Go'nun kendi resmi [web](https://go.dev/doc/modules/layout) sayfasında bununla ilgili bir makale bulunuyor incelemek isterseniz diye ekliyorum.

_Açıklama :_

`app/` içerisinde uygulama ile ilgili özel dosyalarımız olacak route vb.

`configs/` içerisinde ayarlar için kullandığımız database ve .env gibi dosyalar olacak

`controllers/` içerisinde uygulamamıza gelen istekleri yöneten controller'ımız olacak

`migrations/` içerisinde veri tabanımızda kullanacağımız tablo oluşturma komutlarını içeren dosyalar olacak

`models/` içerisinde tablo yapılarımıza ait dosyalar olacak

`repository/` içerisinde veritabanı işlemlerimizi yapacağımız dosyalar olacak

`services/` içerisinde controller ve repository arasında bağlantıyı sağlayan servis dosyalarımız olacak

---

## Go uygulamasını Dockerize edelim

`Dockerfile`'ı dolduralım :

```
FROM golang:1.21.1-alpine3.18

WORKDIR /app

COPY . .

RUN go get -d -v ./...
RUN go install -v ./...

RUN go build -o api-fiber-gorm .

EXPOSE 3000

CMD ["./api-fiber-gorm"]
```

_Açıklama :_

`FROM` kullanılacak temel imajı ayarlar. Biz daha hafif bir sürüm olan **golang:1.21.1-alpine3.18** imajını kullanacağız

`WORKDIR` imaj içindeki çalışma dizinini ayarlar

`COPY` geçerli dizindeki tüm dosyaları çalışma dizinine kopyalar

`RUN go get -d -v` projenin bağımlılıklarını çözer. **-d** bayrağı, bağımlılıkları yalnızca indirir ancak derlemez. **-v** bayrağı, işlemi ayrıntılı bir şekilde görüntüler

`RUN go install -v` projenin ana uygulamasını derler ve **go install** ile bin klasörüne yerleştirir. Bu komut, bağımlılıkları değil, yalnızca ana uygulamayı derler

`RUN go build -o` projenin Docker konteynerine dahil edilmesi ve derlenmiş bir çalıştırılabilir dosyanın oluşturulması için kullanılır

`EXPOSE` Docker konteynerinizin belirli bir portunun diğer konteynerlere veya host işletim sistemine nasıl görünür hale getirileceğini tanımlayan bir Dockerfile komutudur

`CMD` Docker konteyneri çalıştığında çalıştırılacak varsayılan komut veya komutları tanımlamak için kullanılır

---

## Docker Compose

`docker-compose.yml` dosyasını dolduralım :

```
version: '3.8'

services:
  database:
    container_name: go-db
    image: postgres:16.0-alpine3.18
    network_mode: bridge
    restart: always
    env_file:
      - .env
    ports:
      - "5432:5432"
    volumes:
      - data:/var/lib/postgresql/data
  server:
    container_name: go-server
    network_mode: bridge
    build:
      context: ./
      dockerfile: Dockerfile
    env_file:
      - .env
    depends_on:
      - database
    ports:
      - "3000:3000"
volumes:
  data:
```

_Açıklama :_

`database` ve `server` diye 2 hizmet oluşturduk

`version: '3.8'` Docker Compose sürümünü belirtir. Bu, kullanılan Docker Compose sürümüne göre farklılık gösterebilir.

`services` Docker Compose ile yönetilecek hizmetlerin başladığı bölümü tanımlar.

`database` İlk hizmetin adıdır ve bir Docker veritabanı konteyneri oluşturur.

`container_name` Konteynerin adını belirtir. Bu isim, Docker Compose tarafından kullanılır ve benzersiz olmalıdır.

`image` Konteynerin kullanacağı Docker imajını belirtir. Bu durumda, PostgreSQL veritabanı imajı kullanılır.

`network_mode` Konteynerin hangi ağ modunda çalışacağını belirtir. "bridge", varsayılan ağ modudur.

`restart` Konteynerin her zaman yeniden başlatılmasını belirtir.

`env_file` Konteynerin çevresel değişkenlerini .env dosyasından okur.

`ports` Konteynerin hangi portların host işletim sistemine yönlendirileceğini belirtir. Bu durumda, PostgreSQL'in 5432 numaralı portu host işletim sistemi üzerinde 5432 numaralı porta yönlendirilir.

`volumes` Konteynerin hangi disk alanlarını kullanacağını ve bu alanların nasıl paylaşılacağını belirtir. "data" adlı bir Docker volume'u, PostgreSQL veritabanının verilerini saklamak için kullanılır.

`server` İkinci hizmetin adıdır ve bir Go uygulamasını içeren bir Docker konteyneri oluşturur.

`build` Konteynerin Docker imajını oluşturmak için kullanılacak kaynakları tanımlar. Bu durumda, context ve dockerfile seçenekleri ile Dockerfile ve bağlam (context) belirtilir.

`depends_on` Konteynerin başlamadan önce bağımlı olduğu hizmetleri belirtir. Bu durumda, "database" hizmeti başlamadan "server" hizmeti başlamaz.

`volumes` Docker Compose tarafından kullanılacak Docker volume'larını tanımlar. "data" adlı bir volume tanımlanmıştır.

---

## .env

`.env` içerisinde docker üzerinden ayağa kaldıracağımız PostgreSQL veritabanımızın ayar bilgilerini yapacağız.

```
POSTGRES_HOST=localhost
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
POSTGRES_PORT=5432
```

---

## main.go file

main.go dosyası uygulamanın ana dosyasıdır: tüm uç noktaları ve uygulamanın mantığını içerir.

```go
package main

import (
	r "api-fiber-gorm/app"
	"api-fiber-gorm/configs"
	"api-fiber-gorm/controllers"
	"api-fiber-gorm/repository"
	"api-fiber-gorm/services"
	"github.com/gofiber/fiber/v2"
)

func main() {
	app := fiber.New()

	dbClient := configs.ConnectPostgreSQL()

	bookRepoDB := repository.NewBookRepositoryDB(dbClient)
	bookService := services.NewBookService(bookRepoDB)
	bookController := controllers.NewBookController(bookService)

	r.SetupBookRoutes(app, bookController)
	// other routes

	err := app.Listen(":3000")
	if err != nil {
		return
	}
}

```

## Configs

`configs/env.go` dosyası bizim database bilgilerimizi aldığımız yer ve bize bir string değer dönüyor.

```go
package configs

import (
	"fmt"
	"github.com/joho/godotenv"
	"log"
	"os"
)

func EnvPostgreDBUrl() string {
	if err := godotenv.Load(); err != nil {
		log.Println("No .env file found")
	}

	pDBHost := getEnvVar("POSTGRES_HOST")
	pDBUser := getEnvVar("POSTGRES_USER")
	pDBPassword := getEnvVar("POSTGRES_PASSWORD")
	pDBName := getEnvVar("POSTGRES_DB")
	pDBPort := getEnvVar("POSTGRES_PORT")

	pDBUrl := fmt.Sprintf(
		"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
		pDBHost, pDBUser, pDBPassword, pDBName, pDBPort,
	)

	return pDBUrl
}

func getEnvVar(key string) string {
	value := os.Getenv(key)
	if value == "" {
		log.Fatalf("You must set your '%s' environmental variable.", key)
	}

	return value
}

```

`configs/database.go` dosyasında Docker üzerinden ayağa kaldırdığımız PostgreSQL bağlantımızı yaptığımız yer.

```go
package configs

import (
	"api-fiber-gorm/models"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func ConnectPostgreSQL() *gorm.DB {
	dsn := EnvPostgreDBUrl()

	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}

	migrateErr := db.AutoMigrate(&models.Book{})
	if migrateErr != nil {
		panic(migrateErr)
	}

	return db
}

```

## bookModel.go

`models/bookModel.go` içerisinde tablo yapımızı oluşturuyoruz.

```go
package models

import "gorm.io/gorm"

type Book struct {
	gorm.Model

	ID     int    `json:"id" gorm:"primaryKey"`
	Title  string `json:"title"`
	Author string `json:"author"`
}

```

## route.go

`app/route.go` dosyası bizim HTTP isteklerini tanımladığımız yer.

```go
package app

import (
	"api-fiber-gorm/controllers"
	"github.com/gofiber/fiber/v2"
)

func SetupBookRoutes(c *fiber.App, bc controllers.BookController) {
	route := c.Group("/api")

	v1 := route.Group("/v1")
	v1.Get("/", func(c *fiber.Ctx) error {
		return c.SendString("Hello, World!")
	})

	v1.Post("/book", bc.CreateBook)
	...
        ...

	/*
		v2 := route.Group("/v2")
		v2.Get("/", func(c *fiber.Ctx) error {
			return c.SendString("Hello, World!")
		})
	*/
}

/*
func SetupAuthorRoutes(){}
*/

```

## Controller

`controllers/bookController.go` içerisinde uygulama içerisinde hangi istekleri alacağımızı tanımlıyoruz.

```go
package controllers

import (
	"api-fiber-gorm/models"
	"api-fiber-gorm/services"
	"github.com/gofiber/fiber/v2"
	"net/http"
	"strconv"
)

type BookService struct {
	Service services.BookService
}

type BookController interface {
	CreateBook(c *fiber.Ctx) error
	...
        ...
}

func (bs BookService) CreateBook(c *fiber.Ctx) error {
	var book models.Book

	if err := c.BodyParser(&book); err != nil {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"error":   "Invalid Json.",
			"message": err.Error(),
		})
	}

	err := bs.Service.BookInsert(book)
	if err != nil {
		return c.Status(http.StatusBadGateway).JSON(fiber.Map{
			"error":   "Service unavailable.",
			"message": err.Error(),
		})
	}

	return c.Status(http.StatusCreated).JSON(fiber.Map{
		"message": "Book created successfully.",
	})
}

...
...
...

func NewBookController(service services.BookService) *BookService {
	return &BookService{service}
}

```

## Services

`services/bookService.go` içerisinde `bookController.go` içerisinde tanımlı methodlardan gelen talepleri karşılıyoruz.

```go
package services

import (
	"api-fiber-gorm/models"
	"api-fiber-gorm/repository"
)

type BookRepository struct {
	Repository repository.BookRepository
}

type BookService interface {
	BookInsert(book models.Book) error
	...
        ...
}

func (br BookRepository) BookInsert(book models.Book) error {
	err := br.Repository.Insert(book)
	if err != nil {
		return err
	}

	return nil
}

...
...
...

func NewBookService(repository repository.BookRepository) *BookRepository {
	return &BookRepository{repository}
}

```

## Repository

`repository/bookRepository.go` içerisinde veritabanı işlemlerimizi gerçekleştiriyoruz. 

```go
package repository

import (
	"api-fiber-gorm/models"
	"gorm.io/gorm"
	"log"
)

type BookDB struct {
	DB *gorm.DB
}

type BookRepository interface {
	Insert(book models.Book) error
	...
        ...
}

func (bdb BookDB) Insert(book models.Book) error {
	if result := bdb.DB.Create(&book); result.Error != nil {
		log.Printf("bookRepository Insert error : %s", result.Error)

		return result.Error
	}

	return nil
}

...
...
...

func NewBookRepositoryDB(dbClient *gorm.DB) *BookDB {
	return &BookDB{dbClient}
}

```

## Uygulamayı başlatma

Terminal ekranından `docker compose up` diyerek servislerimizi başlatıyoruz. İlk defa bu işlemi yapıyorsak aşağıdaki gibi bir görüntü ile karşılaşacaksınız.


![Docker compose up install image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tuq98jlbjared8rd7l6b.png)

![Docker compose up](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/214dvke4cj3koobu68is.png) 

Bu işlemler tamamlandıktan sonra Docker Desktop uygulamasını açıp aşağıdaki gibi container'larınızı görebilirsiniz.

![Docker Desktop Container](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k7wfup0gmlr9vh4akjof.png)

## Test Etme

Uygulamamız ayağa kalktığında aşağıdaki gibi bir görüntü ile karşılaşacaksınız.

![Go App Running](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pjtijakjj6itixrumyc4.png)

Daha sonra `route.go` dosyamızda tanımladığımız kitap oluşturma adresine istek atmak için Postman uygulamasını açıyoruz ve aşağıdaki gibi örnek bir istek atıyoruz.

![Postman Crud](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ps0pcqqu2j2mm21spwf.png)

Daha sonra ilgili kaydımızın doğru bir şekilde eklendiğini kontrol etmek için veri tabanına bakacağız. Ben burda `TablePlus` kullandım siz farklı bir sql editörü kullanabilirsiniz.

![TablePlus record](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a2u6yt3hchc8an2vghqc.png)

---

## Sonuç

Ve artık örnek bir REST API projesi nasıl yapılır ve nasıl dockerize ediliri bunu öğrenmiş olduk. Burada anlattıklarım benim kendi yöntemim diyebilirim ve kesinlikle böyle yapmak zorundasınız demiyorum.

**Bu makaledeki tüm kodlara [buradan](https://github.com/yunusemremert/api-fiber-gorm) ulaşabilirsiniz.** 

Bu makaleyi yazmadan önceki yaptığım 3 çalışmanın da linklerini aşağıya bırakıyorum incelemek isterseniz diye.

[Golang Api Tutorial for Fiber & MongoDB](https://github.com/yunusemremert/golang-api)
[Golang Api Tutorial for Gin](https://github.com/yunusemremert/api-gin)
[Golang Api Tutorial for Gin & Gorm (PostgreSQL)](https://github.com/yunusemremert/api-gin-gorm)

Sağlıcakla, sevgilerle...