Fiber #

Fiber adalah framework web untuk Go yang terinspirasi dari Express.js. API-nya terasa familiar untuk developer Node.js — middleware, routing dengan app.Get("/path", handler), dan c.JSON() untuk response — tapi performanya setara Gin karena dibangun di atas fasthttp (bukan net/http standar). Untuk tim yang pindah dari Node.js ke Go, Fiber menawarkan ergonomics yang jauh lebih ramah dibanding Gin atau net/http standar.

Artikel ini membahas setup lengkap Docker Compose untuk local development Fiber, dari Dockerfile multi-stage, hot reload dengan Air, hingga integrasi database dan best practice. Setelah membaca artikel ini, kamu akan punya template yang bisa langsung dipakai untuk project Fiber baru.

Prasyarat #

Pastikan sudah terinstall:

  • Docker dan Docker Compose versi terbaru
  • Go 1.22+ (opsional, untuk development di host)
  • Git

Struktur project Fiber standar:

my-fiber-app/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── handler/
│   │   └── user.go
│   ├── service/
│   │   └── user.go
│   ├── repository/
│   │   └── user.go
│   └── model/
│       └── user.go
├── go.mod
├── go.sum
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── .air.toml
├── .env
└── .dockerignore

Perbedaan Mendasar Fiber vs Gin #

Sebelum masuk ke teknis, pahami dulu kenapa Fiber berbeda dari Gin. Ini mempengaruhi banyak keputusan di Dockerfile dan Compose.

Aspek Gin Fiber
HTTP engine net/http standar fasthttp (custom)
API style Express-like, idiomatic Go Express-like, lebih dekat ke JS
Middleware gin.HandlerFunc fiber.Handler (fungsi biasa)
Konteks *gin.Context *fiber.Ctx (method-rich)
Body binding c.ShouldBindJSON c.BodyParser
Static file r.Static("/uploads", "./uploads") app.Static("/uploads", "./uploads")
Performa Sangat cepat Sedikit lebih cepat di benchmark sintetis
Kompabilitas middleware net/http Ya Tidak (perlu adapter)

Karena Fiber pakai fasthttp (bukan net/http), middleware net/http standar tidak bisa dipakai langsung. Kamu perlu adapter github.com/gofiber/adaptor untuk pakai middleware seperti gorilla/handlers atau prometheus/client_golang. Untuk kebanyakan aplikasi hijau, hal ini bukan masalah — middleware Fiber sudah cukup lengkap.

Dockerfile Multi-Stage #

Pattern multi-stage sama dengan Go pada umumnya: builder stage untuk kompilasi, runtime stage ramping.

# syntax=docker/dockerfile:1.6
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Install tools OS yang dibutuhkan saat build
RUN apk add --no-cache git ca-certificates

# Cache dependency
COPY go.mod go.sum ./
RUN go mod download

# Build binary statis
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/server ./cmd/server

# Stage kedua: runtime image ramping
FROM alpine:3.19

RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app

# Non-root user
RUN addgroup -S app && adduser -S app -G app
USER app:app

COPY --from=builder /out/server /app/server

EXPOSE 3000

ENTRYPOINT ["/app/server"]

Image akhir < 20 MB. Cocok untuk di-deploy ke Cloud Run, Fly.io, atau Kubernetes.

Dockerfile untuk Development #

Untuk development dengan hot reload, pakai Air.

# Dockerfile.dev
FROM golang:1.22-alpine

WORKDIR /app

RUN apk add --no-cache git curl bash

# Install Air
RUN go install github.com/air-verse/air@latest

# Cache dependency
COPY go.mod go.sum ./
RUN go mod download

EXPOSE 3000

CMD ["air", "-c", ".air.toml"]

Konfigurasi Air (.air.toml):

root = "."
tmp_dir = "tmp"

[build]
  bin = "./tmp/main"
  cmd = "go build -o ./tmp/main ./cmd/server"
  delay = 1000
  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
  include_dir = ["cmd", "internal"]
  include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml"]
  exclude_regex = ["_test.go"]
  exclude_unchanged = true

[log]
  time = true

[misc]
  clean_on_exit = true

docker-compose.yml #

# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: fiber-app:dev
    container_name: fiber-api
    ports:
      - "3000:3000"
    volumes:
      - ./cmd:/app/cmd
      - ./internal:/app/internal
      - ./go.mod:/app/go.mod
      - ./go.sum:/app/go.sum
      - go-build:/app/tmp
    environment:
      - APP_ENV=development
      - SERVER_PORT=3000
      - DATABASE_URL=postgres://app:dev@db:5432/fiberapp?sslmode=disable
      - REDIS_URL=redis://cache:6379/0
      - JWT_SECRET=local-dev-secret-change-me
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy
    command: air -c .air.toml

  db:
    image: postgres:16-alpine
    container_name: fiber-db
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=dev
      - POSTGRES_DB=fiberapp
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d fiberapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    container_name: fiber-cache
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    volumes:
      - cache-data:/data
    ports:
      - "6379:6379"

volumes:
  db-data:
  cache-data:
  go-build:

Contoh Aplikasi Fiber #

cmd/server/main.go:

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/gofiber/fiber/v2/middleware/recover"
    "github.com/gofiber/fiber/v2/middleware/requestid"
    "github.com/redis/go-redis/v9"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"

    "myapp/internal/handler"
    "myapp/internal/model"
    "myapp/internal/repository"
    "myapp/internal/service"
)

func main() {
    // Database dengan retry
    var db *gorm.DB
    var err error
    dsn := os.Getenv("DATABASE_URL")
    for i := 0; i < 30; i++ {
        db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err == nil {
            break
        }
        log.Printf("waiting for db: %v", err)
        time.Sleep(2 * time.Second)
    }
    if err != nil {
        log.Fatal("db connection failed: ", err)
    }
    if err := db.AutoMigrate(&model.User{}); err != nil {
        log.Fatal("migrate failed: ", err)
    }

    // Redis
    rdb := redis.NewClient(&redis.Options{
        Addr: os.Getenv("REDIS_URL"),
    })

    // Layers
    userRepo := repository.NewUserRepository(db)
    userSvc := service.NewUserService(userRepo)
    userHandler := handler.NewUserHandler(userSvc)

    // Fiber app
    app := fiber.New(fiber.Config{
        AppName:               "my-fiber-app",
        DisableStartupMessage: false,
        ErrorHandler: func(c *fiber.Ctx, err error) error {
            code := fiber.StatusInternalServerError
            if e, ok := err.(*fiber.Error); ok {
                code = e.Code
            }
            return c.Status(code).JSON(fiber.Map{"error": err.Error()})
        },
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    })

    // Middleware
    app.Use(requestid.New())
    app.Use(logger.New(logger.Config{
        Format:     "${time} ${status} ${method} ${path} ${latency} ${locals:requestid}\n",
        TimeFormat: "2006-01-02T15:04:05Z07:00",
    }))
    app.Use(recover.New())
    app.Use(cors.New(cors.Config{
        AllowOrigins: "*",
        AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
        AllowHeaders: "Content-Type,Authorization,X-Request-ID",
    }))

    // Health
    app.Get("/health", func(c *fiber.Ctx) error {
        return c.JSON(fiber.Map{
            "status": "ok",
            "time":   time.Now().Format(time.RFC3339),
        })
    })

    // Routes
    api := app.Group("/api/v1")
    api.Post("/users", userHandler.Create)
    api.Get("/users/:id", userHandler.Get)

    // Start server dengan graceful shutdown
    go func() {
        if err := app.Listen(":3000"); err != nil {
            log.Printf("server stopped: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("shutting down...")

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := app.ShutdownWithContext(ctx); err != nil {
        log.Fatal(err)
    }
}

Handler (internal/handler/user.go):

package handler

import (
    "errors"
    "strconv"

    "github.com/gofiber/fiber/v2"

    "myapp/internal/service"
)

type UserHandler struct {
    svc *service.UserService
}

func NewUserHandler(svc *service.UserService) *UserHandler {
    return &UserHandler{svc: svc}
}

type createUserRequest struct {
    Email    string `json:"email"`
    Name     string `json:"name"`
    Password string `json:"password"`
}

type errorResponse struct {
    Error string `json:"error"`
}

func (h *UserHandler) Create(c *fiber.Ctx) error {
    var req createUserRequest
    if err := c.BodyParser(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(errorResponse{Error: err.Error()})
    }
    if req.Email == "" || req.Name == "" || len(req.Password) < 8 {
        return c.Status(fiber.StatusBadRequest).JSON(errorResponse{
            Error: "email, name required and password min 8 chars",
        })
    }
    u, err := h.svc.Register(c.UserContext(), req.Email, req.Name, req.Password)
    if err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(errorResponse{Error: err.Error()})
    }
    return c.Status(fiber.StatusCreated).JSON(u)
}

func (h *UserHandler) Get(c *fiber.Ctx) error {
    id, err := strconv.ParseUint(c.Params("id"), 10, 64)
    if err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(errorResponse{Error: "invalid id"})
    }
    u, err := h.svc.Get(c.UserContext(), id)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(errorResponse{Error: err.Error()})
    }
    if u == nil {
        return c.Status(fiber.StatusNotFound).JSON(errorResponse{Error: "user not found"})
    }
    return c.JSON(u)
}

Service (internal/service/user.go):

package service

import (
    "context"
    "errors"
    "strings"

    "golang.org/x/crypto/bcrypt"

    "myapp/internal/model"
    "myapp/internal/repository"
)

type UserService struct {
    repo *repository.UserRepository
}

func NewUserService(repo *repository.UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) Register(ctx context.Context, email, name, password string) (*model.User, error) {
    email = strings.ToLower(strings.TrimSpace(email))
    existing, _ := s.repo.FindByEmail(ctx, email)
    if existing != nil {
        return nil, errors.New("email already registered")
    }
    hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return nil, err
    }
    u := &model.User{
        Email:    email,
        Name:     strings.TrimSpace(name),
        Password: string(hashed),
    }
    if err := s.repo.Create(ctx, u); err != nil {
        return nil, err
    }
    return u, nil
}

func (s *UserService) Get(ctx context.Context, id uint64) (*model.User, error) {
    return s.repo.FindByID(ctx, id)
}

Repository (internal/repository/user.go):

package repository

import (
    "context"
    "errors"

    "gorm.io/gorm"

    "myapp/internal/model"
)

type UserRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) Create(ctx context.Context, u *model.User) error {
    return r.db.WithContext(ctx).Create(u).Error
}

func (r *UserRepository) FindByID(ctx context.Context, id uint64) (*model.User, error) {
    var u model.User
    err := r.db.WithContext(ctx).First(&u, id).Error
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, nil
    }
    return &u, err
}

func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*model.User, error) {
    var u model.User
    err := r.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, nil
    }
    return &u, err
}

Model (internal/model/user.go):

package model

import "time"

type User struct {
    ID        uint64    `gorm:"primaryKey" json:"id"`
    Email     string    `gorm:"size:255;not null;uniqueIndex" json:"email"`
    Name      string    `gorm:"size:255;not null" json:"name"`
    Password  string    `gorm:"size:255;not null" json:"-"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

func (User) TableName() string {
    return "users"
}

Middleware Bawaan Fiber #

Fiber punya banyak middleware resmi yang siap pakai:

Middleware Fungsi
recover Tangkap panic, return 500
logger Log setiap request
requestid Generate/extract request ID
cors Handle CORS
compress Gzip/deflate response
limiter Rate limit
jwt Validasi JWT
basicauth HTTP Basic Auth
cache HTTP cache header
etag ETag header
monitor Endpoint metrics (butuh ORM)
csrf CSRF token
helmet Security header
healthcheck Liveness/readiness endpoint

Contoh kombinasi middleware yang aman untuk API:

app.Use(requestid.New())
app.Use(recover.New())
app.Use(logger.New())
app.Use(cors.New())
app.Use(helmet.New())
app.Use(compress.New())
app.Use(etag.New())

helmet mengatur security header (X-Frame-Options, X-Content-Type-Options, dll). compress mengurangi ukuran response. etag mengaktifkan HTTP caching.

Validasi dengan go-playground/validator #

Sama seperti Gin, Fiber juga menggunakan go-playground/validator. Tapi tag-nya bukan binding, melainkan validate.

import "github.com/go-playground/validator/v10"

var validate = validator.New()

type createUserRequest struct {
    Email    string   `json:"email" validate:"required,email"`
    Name     string   `json:"name" validate:"required,min=2,max=100"`
    Password string   `json:"password" validate:"required,min=8,max=72"`
    Roles    []string `json:"roles" validate:"required,min=1,dive,oneof=admin user guest"`
    Age      int      `json:"age" validate:"gte=18,lte=120"`
}

func (h *UserHandler) Create(c *fiber.Ctx) error {
    var req createUserRequest
    if err := c.BodyParser(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": err.Error()})
    }
    if err := validate.Struct(req); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": err.Error()})
    }
    // lanjut
}

Build dan Run #

# Build image development
docker compose build

# Jalankan semua service
docker compose up -d

# Lihat log
docker compose logs -f api

# Shell ke dalam container
docker compose exec api sh

# Stop
docker compose down

# Reset total (hapus volume)
docker compose down -v

Akses:

  • API: http://localhost:3000
  • Health check: http://localhost:3000/health
  • PostgreSQL: localhost:5432
  • Redis: localhost:6379

Test:

# Register
curl -X POST http://localhost:3000/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Siti","password":"password123"}'

# Get user
curl http://localhost:3000/api/v1/users/1

WebSocket dengan Fiber #

Fiber punya support WebSocket built-in lewat middleware github.com/gofiber/contrib/websocket.

import "github.com/gofiber/contrib/websocket"

app.Get("/ws", websocket.New(func(c *websocket.Conn) {
    defer c.Close()
    for {
        mt, msg, err := c.ReadMessage()
        if err != nil {
            return
        }
        log.Printf("recv: %s", msg)
        if err := c.WriteMessage(mt, msg); err != nil {
            return
        }
    }
}))

Untuk real-time chat atau notification, WebSocket di Fiber sangat mudah di-setup. Alternatifnya pakai Server-Sent Events (SSE) untuk one-way communication.

File Upload dan Static File #

// Single file upload
app.Post("/upload", func(c *fiber.Ctx) error {
    file, err := c.FormFile("file")
    if err != nil {
        return c.Status(400).JSON(fiber.Map{"error": err.Error()})
    }
    if file.Size > 5*1024*1024 {
        return c.Status(413).JSON(fiber.Map{"error": "file too large"})
    }
    filename := fmt.Sprintf("./uploads/%d_%s", time.Now().UnixNano(), file.Filename)
    if err := c.SaveFile(file, filename); err != nil {
        return c.Status(500).JSON(fiber.Map{"error": err.Error()})
    }
    return c.JSON(fiber.Map{"filename": filename, "size": file.Size})
})

// Serve static
app.Static("/uploads", "./uploads")
app.Static("/", "./public")

Testing dengan Testcontainers #

Sama dengan Gin, Fiber cocok dengan Testcontainers untuk integration test.

package handler_test

import (
    "bytes"
    "encoding/json"
    "io"
    "net/http/httptest"
    "testing"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/stretchr/testify/assert"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"

    "myapp/internal/handler"
    "myapp/internal/model"
    "myapp/internal/repository"
    "myapp/internal/service"
    "context"
)

func setupApp(t *testing.T) (*fiber.App, func()) {
    ctx := context.Background()
    pgC, err := postgres.RunContainer(ctx,
        postgres.WithDatabase("test"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
    )
    if err != nil {
        t.Fatal(err)
    }
    dsn, _ := pgC.ConnectionString(ctx, "sslmode=disable")
    db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    db.AutoMigrate(&model.User{})

    repo := repository.NewUserRepository(db)
    svc := service.NewUserService(repo)
    h := handler.NewUserHandler(svc)

    app := fiber.New()
    app.Post("/users", h.Create)
    app.Get("/users/:id", h.Get)

    cleanup := func() {
        pgC.Terminate(ctx)
    }
    return app, cleanup
}

func TestCreateUser(t *testing.T) {
    app, cleanup := setupApp(t)
    defer cleanup()

    body := map[string]string{
        "email":    "[email protected]",
        "name":     "Test",
        "password": "password123",
    }
    b, _ := json.Marshal(body)
    req := httptest.NewRequest("POST", "/users", bytes.NewReader(b))
    req.Header.Set("Content-Type", "application/json")

    resp, err := app.Test(req, -1)
    assert.NoError(t, err)
    assert.Equal(t, 201, resp.StatusCode)

    bodyBytes, _ := io.ReadAll(resp.Body)
    assert.Contains(t, string(bodyBytes), "[email protected]")
}

app.Test() menjalankan handler di-memory tanpa perlu listen ke port — sangat cepat untuk test.

Kapan Fiber Tepat, Kapan Tidak #

Pakai Fiber jika:
  ✓ Developer familiar dengan Express.js
  ✓ Butuh WebSocket built-in
  ✓ Suka API yang lebih deklaratif
  ✓ Microservice dengan throughput tinggi

Hindari Fiber jika:
  ✗ Butuh middleware net/http standar
  ✗ Library ekosistem (mis. OpenTelemetry SDK) wajib pakai net/http
  ✗ Tim kuat di Go idiomatic dan lebih suka net/http
  ✗ gRPC service

Karena Fiber pakai fasthttp, ada beberapa library Go yang tidak kompatibel. Contoh: prometheus/client_golang (untuk metrics), otelhttp (untuk tracing), dan beberapa middleware observability. Untuk pakai library tersebut, kamu perlu adapter atau implementasi custom.

Best Practice #

Pakai Air untuk Hot Reload #

Tanpa hot reload, Go terasa lambat untuk iterasi. Air memantau file .go dan rebuild otomatis. Pasang via go install di Dockerfile.dev.

Cache Go Module #

Selalu copy go.mod + go.sum lebih dulu, jalankan go mod download. Layer dependency jarang berubah, jadi cache lebih sering hit.

Bind Mount Source Code #

Di development, mount source code ke container. Volume go-build cache hasil build agar restart container lebih cepat.

Healthcheck dan Retry Loop #

PostgreSQL dan Redis harus punya healthcheck. Service Fiber tidak boleh start sebelum db dan cache healthy. Tambahkan retry loop di kode Go untuk koneksi awal.

Pisahkan Config dari Code #

Pakai os.Getenv() untuk semua konfigurasi. Sediakan .env.example di repo. Jangan hardcode URL, port, atau secret.

Pakai fasthttp dengan Sadar #

Fiber pakai fasthttp — ia tidak kompatibel dengan library net/http standar tanpa adaptor. Untuk metrics, tracing, dan middleware ekosistem, cek dulu apakah ada adapter Fiber-nya.

Graceful Shutdown #

Tambahkan signal handler agar server bisa shutdown dengan bersih saat container di-restart. Tanpa ini, koneksi aktif bisa terputus.

Troubleshooting #

Air Tidak Detect Perubahan #

Pastikan bind mount include direktori cmd/ dan internal/. Di Mac/Windows dengan Docker Desktop, kadang butuh beberapa detik. Naikkan delay di .air.toml ke 2000-3000 ms kalau rebuild terlalu sering.

Database Connection Refused #

Pakai depends_on: condition: service_healthy di Compose, dan tambahkan retry loop di kode Go. depends_on saja tidak cukup karena Docker hanya menunggu container start, bukan service siap.

Middleware net/http Tidak Berfungsi #

Fiber pakai fasthttp, jadi middleware net/http standar tidak bisa dipakai langsung. Cari versi Fiber-nya di github.com/gofiber/contrib atau tulis custom.

Port 3000 Sudah Dipakai #

lsof -i :3000
# atau
netstat -ano | findstr :3000

Stop proses atau ubah mapping di Compose ("3001:3000").

Memory Leak #

Pastikan setiap request yang allocate resource besar di-close dengan benar. Untuk koneksi database, pakai connection pool (sql.DB untuk database/sql, atau konfigurasi pool di GORM).

Ringkasan #

  • Fiber ideal untuk developer Node.js yang pindah ke Go — API Express-like, ergonomics familiar, performa setara Gin.
  • Fiber pakai fasthttp, bukan net/http. Ini membuat middleware ekosistem Go standar tidak langsung kompatibel. Untuk observability dan metrics, perlu adapter atau implementasi custom.
  • Dockerfile multi-stage untuk production: builder stage dengan Go toolchain, runtime stage ramping dengan alpine. Image akhir < 20 MB.
  • Dockerfile.dev untuk development: full toolchain + Air untuk hot reload.
  • Hot reload dengan Air: pantau file .go, rebuild, restart. Konfigurasi lewat .air.toml.
  • Layer cache Go: copy manifest dulu, baru source code. Build inkremental super cepat.
  • Middleware built-in: recover, logger, requestid, cors, compress, etag, helmet, jwt, limiter. Tinggal app.Use().
  • WebSocket built-in lewat github.com/gofiber/contrib/websocket — sangat mudah untuk real-time app.
  • File upload lewat c.FormFile() dan c.SaveFile(). Static file lewat app.Static().
  • Validasi dengan go-playground/validator lewat tag validate. Library validator terpisah, bukan built-in.
  • Healthcheck untuk service dependent: Postgres pg_isready, Redis redis-cli ping. Pakai depends_on: condition: service_healthy di Compose.
  • Testcontainers untuk integration test: spin up Postgres container, jalankan test, cleanup otomatis.
  • Pakai Fiber jika familiar dengan Express.js, butuh WebSocket, atau suka API deklaratif. Hindari jika ekosistem net/http adalah hard requirement.
  • Best practice: pisahkan Dockerfile dev & prod, cache dependency, bind mount source, healthcheck, retry loop, graceful shutdown, sadar kompatibilitas fasthttp.
  • Alternatif: Gin untuk kompatibilitas net/http, chi untuk router ringan, net/http standar untuk service minimal.

← Sebelumnya: Gin   Berikutnya: Chi →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact