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.

AspekGinFiber
HTTP enginenet/http standarfasthttp (custom)
API styleExpress-like, idiomatic GoExpress-like, lebih dekat ke JS
Middlewaregin.HandlerFuncfiber.Handler (fungsi biasa)
Konteks*gin.Context*fiber.Ctx (method-rich)
Body bindingc.ShouldBindJSONc.BodyParser
Static filer.Static("/uploads", "./uploads")app.Static("/uploads", "./uploads")
PerformaSangat cepatSedikit lebih cepat di benchmark sintetis
Kompabilitas middleware net/httpYaTidak (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:

MiddlewareFungsi
recoverTangkap panic, return 500
loggerLog setiap request
requestidGenerate/extract request ID
corsHandle CORS
compressGzip/deflate response
limiterRate limit
jwtValidasi JWT
basicauthHTTP Basic Auth
cacheHTTP cache header
etagETag header
monitorEndpoint metrics (butuh ORM)
csrfCSRF token
helmetSecurity header
healthcheckLiveness/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