Yunus Emre MERT 🦁 🇹🇷
Posted on October 7, 2023
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
- Fiber (Web Framework & Web Server)
- PostgreSQL (Veritabanı)
- GORM (ORM)
- Docker (Konteynerizasyon için)
- 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.
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"]
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.
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.
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.
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.
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.
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.
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.
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.
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.
Bu işlemler tamamlandıktan sonra Docker Desktop uygulamasını açıp aşağıdaki gibi container'larınızı görebilirsiniz.
Test Etme
Uygulamamız ayağa kalktığında aşağıdaki gibi bir görüntü ile karşılaşacaksınız.
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.
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.
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...
Posted on October 7, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.