Gorm, Fiber, PostgreSQL ve Docker kullanarak Golang REST API

yunusemremert

Yunus Emre MERT 🦁 🇹🇷

Posted on October 7, 2023

Gorm, Fiber, PostgreSQL ve Docker kullanarak Golang REST API

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 (Web Framework & Web Server)
  2. PostgreSQL (Veritabanı)
  3. GORM (ORM)
  4. Docker (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

Not : Bu sadece örnek bir yapıdır, isterseniz kendi proje yapınızı oluşturabilirsiniz! Hatta Go'nun kendi resmi web 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"]
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

main.go file

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

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
    }
}

Enter fullscreen mode Exit fullscreen mode

Configs

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

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
}

Enter fullscreen mode Exit fullscreen mode

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

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
}

Enter fullscreen mode Exit fullscreen mode

bookModel.go

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

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"`
}

Enter fullscreen mode Exit fullscreen mode

route.go

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

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(){}
*/

Enter fullscreen mode Exit fullscreen mode

Controller

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

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}
}

Enter fullscreen mode Exit fullscreen mode

Services

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

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}
}

Enter fullscreen mode Exit fullscreen mode

Repository

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

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}
}

Enter fullscreen mode Exit fullscreen mode

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

Docker compose up

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

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

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

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


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 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
Golang Api Tutorial for Gin
Golang Api Tutorial for Gin & Gorm (PostgreSQL)

Sağlıcakla, sevgilerle...

💖 💪 🙅 🚩
yunusemremert
Yunus Emre MERT 🦁 🇹🇷

Posted on October 7, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related