@sedatdemirAra

Go 1.26 web servisi: stdlib router ve Huma

Go'nun standart kütüphanesiyle HTTP sunucusu yazmak her zaman mümkündü ama yıllarca aynı sıkıntı tekrarlandı: net/http'nin varsayılan mux'ı path parametresi desteklemiyordu. Bir /users/{id} rotası tanımlamak için ya regex yazılıyordu ya da Gorilla Mux, Chi gibi üçüncü parti paketlere başvuruluyordu. Go 1.22 ile net/http.ServeMux'a method ve path pattern desteği geldi; Go 1.24'te küçük iyileştirmeler eklendi. Go 1.26 (Ağustos 2025 itibarıyla) bu altyapıyı olgunlaştırarak harici router'a neredeyse hiç ihtiyaç bırakmıyor.

Bu yazıda iki şeyi bir araya getiriyorum: Go 1.26 stdlib router ile rota tanımlamak ve Huma v2 kütüphanesi ile bu rotalardan otomatik OpenAPI 3.1 dokümantasyonu üretmek.

Stdlib router'ın kısa tarihi

Go 1.22 öncesinde http.ServeMux yalnızca sabit prefix eşlemesi yapıyordu. Bir GET /users/42 isteğini yakalamak için ya tek bir handler içinde strings.Split ile path parse ediliyordu ya da Chi benzeri bir router kullanılıyordu.

Go 1.22'de kabul edilen proposal (GitHub issue #61410) ile ServeMux'a iki yetenek eklendi:

  1. Method prefix'i: "GET /users" şeklinde method belirtme
  2. Path parametresi: "GET /users/{id}" şeklinde wildcard tanımlama, r.PathValue("id") ile okuma

Go 1.26'da bu yapı değişmedi ama derleyici ve runtime tarafında küçük performans iyileştirmeleri var. Daha da önemlisi, ekosistem bu pattern'e adapte oldu: Huma v2, stdlib adapter'ı ile doğrudan http.ServeMux üzerinde çalışabiliyor.

İlk adım: saf stdlib ile bir API

Üçüncü parti paket olmadan, Go 1.26 stdlib ile basit bir CRUD API'si şöyle görünüyor:

package main

import (
	"encoding/json"
	"net/http"
	"sync"
)

type Book struct {
	ID    string `json:"id"`
	Title string `json:"title"`
}

var (
	books = map[string]Book{}
	mu    sync.RWMutex
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /books/{id}", func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		mu.RLock()
		b, ok := books[id]
		mu.RUnlock()
		if !ok {
			http.Error(w, "not found", http.StatusNotFound)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(b)
	})

	mux.HandleFunc("POST /books", func(w http.ResponseWriter, r *http.Request) {
		var b Book
		if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		mu.Lock()
		books[b.ID] = b
		mu.Unlock()
		w.WriteHeader(http.StatusCreated)
	})

	http.ListenAndServe(":8080", mux)
}

Bu kod çalışıyor, bağımlılık sıfır. Ama iki sorun var: input validation elle yapılıyor ve API dokümantasyonu yok. Swagger UI görmek isteyen frontend ekibine bir JSON dosyası elle yazmak gerekiyor. İşte Huma burada devreye giriyor.

Huma v2 nedir ve neden stdlib ile kullanılır

Huma, Go handler'larından OpenAPI 3.1 şeması üreten bir kütüphane. Chi, Fiber, Echo gibi router'lar için adapter'ları var ama Go 1.22+ stdlib ServeMux için de resmi adapter sunuyor. Huma'nın yaklaşımı şu: handler fonksiyonlarını generics ile tanımlıyorsunuz, input/output struct'larına Go struct tag'leri ekliyorsunuz, Huma bunlardan hem validation hem OpenAPI şeması üretiyor.

Üçüncü parti router'a gerek kalmaması, dependency sayısını düşürüyor. Huma v2.32+ sürümlerinde humastdlib adapter'ı stabil.

Proje kurulumu

mkdir bookapi && cd bookapi
go mod init bookapi
go get github.com/danielgtaylor/huma/v2@latest

Go 1.26 gerekiyor. go.mod dosyasında go 1.26 satırını görmelisiniz.

Huma ile OpenAPI'li API yazmak

Aşağıdaki örnekte stdlib router üzerine Huma bağlıyorum, iki endpoint tanımlıyorum ve /docs yolunda Swagger UI sunuyorum.

package main

import (
	"context"
	"fmt"
	"net/http"
	"sync"

	"github.com/danielgtaylor/huma/v2"
	"github.com/danielgtaylor/huma/v2/adapters/humastdlib"
)

// Input ve output struct'ları

type GetBookInput struct {
	ID string `path:"id" required:"true" doc:"Kitabın benzersiz ID'si"`
}

type BookOutput struct {
	Body struct {
		ID    string `json:"id" doc:"Kitap ID"`
		Title string `json:"title" doc:"Kitap başlığı"`
	}
}

type CreateBookInput struct {
	Body struct {
		ID    string `json:"id" required:"true" minLength:"1" doc:"Kitap ID"`
		Title string `json:"title" required:"true" minLength:"1" doc:"Kitap başlığı"`
	}
}

type CreateBookOutput struct {
	Body struct {
		ID string `json:"id"`
	}
}

// Basit in-memory store
var (
	books = map[string]string{}
	mu    sync.RWMutex
)

func main() {
	mux := http.NewServeMux()
	api := humastdlib.New(mux, huma.DefaultConfig("Book API", "1.0.0"))

	huma.Register(api, huma.Operation{
		OperationID: "get-book",
		Method:      http.MethodGet,
		Path:        "/books/{id}",
		Summary:     "Kitap getir",
		Tags:        []string{"books"},
	}, func(ctx context.Context, input *GetBookInput) (*BookOutput, error) {
		mu.RLock()
		title, ok := books[input.ID]
		mu.RUnlock()
		if !ok {
			return nil, huma.Error404NotFound("kitap bulunamadı")
		}
		resp := &BookOutput{}
		resp.Body.ID = input.ID
		resp.Body.Title = title
		return resp, nil
	})

	huma.Register(api, huma.Operation{
		OperationID:   "create-book",
		Method:        http.MethodPost,
		Path:          "/books",
		Summary:       "Yeni kitap oluştur",
		Tags:          []string{"books"},
		DefaultStatus: http.StatusCreated,
	}, func(ctx context.Context, input *CreateBookInput) (*CreateBookOutput, error) {
		mu.Lock()
		books[input.Body.ID] = input.Body.Title
		mu.Unlock()
		resp := &CreateBookOutput{}
		resp.Body.ID = input.Body.ID
		return resp, nil
	})

	fmt.Println("Sunucu :8080 portunda çalışıyor")
	fmt.Println("OpenAPI JSON: http://localhost:8080/openapi.json")
	fmt.Println("Swagger UI:   http://localhost:8080/docs")
	http.ListenAndServe(":8080", mux)
}

Bu kodu çalıştırdığınızda http://localhost:8080/docs adresinde Swagger UI açılıyor. http://localhost:8080/openapi.json adresinde ise tam OpenAPI 3.1 şeması JSON olarak sunuluyor. Hiçbir ek konfigürasyon, hiçbir YAML dosyası yazmadık.

Struct tag'leri ile validation

Huma, struct tag'lerinden hem OpenAPI şeması hem de runtime validation üretiyor. Birkaç kullanışlı tag:

type CreateBookInput struct {
	Body struct {
		ID    string `json:"id" required:"true" minLength:"1" maxLength:"36" pattern:"^[a-z0-9-]+$"`
		Title string `json:"title" required:"true" minLength:"1" maxLength:"200"`
		Year  int    `json:"year" minimum:"1450" maximum:"2030"`
	}
}

required, minLength, maxLength, minimum, maximum, pattern tag'leri doğrudan JSON Schema spec'ine karşılık geliyor. Huma, handler'a ulaşmadan önce isteği bu kurallara göre doğruluyor. Validation hatası olduğunda otomatik olarak RFC 9457 (Problem Details) formatında 422 yanıtı dönüyor:

{
  "type": "https://httpproblems.com/httpstatus/422",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "validation failed",
  "errors": [
    {
      "message": "expected length >= 1",
      "location": "body.title",
      "value": ""
    }
  ]
}

Bu, Zod v4 veya ArkType ile TypeScript'te yaptığımız runtime validation'ın Go karşılığı. Tek fark: tip tanımı ve validation kuralları aynı struct'ta, ayrı bir şema dosyası yok.

Middleware ekleme

Stdlib router kullandığımız için middleware de stdlib pattern'iyle ekleniyor. Huma'nın kendi middleware sistemi var ama standart http.Handler wrapper'ları da çalışıyor:

func requestLogger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Printf("%s %s\n", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux()
	api := humastdlib.New(mux, huma.DefaultConfig("Book API", "1.0.0"))

	// ... endpoint kayıtları ...

	handler := requestLogger(mux)
	http.ListenAndServe(":8080", handler)
}

Rate limiting gibi cross-cutting concern'ler için de aynı pattern geçerli. Middleware zincirini handler = rateLimiter(requestLogger(mux)) şeklinde kurabilirsiniz.

Huma middleware'i

Huma'nın kendi middleware API'si, OpenAPI metadata'sına erişim gerektiren durumlar için var:

api.UseMiddleware(func(ctx huma.Context, next func(huma.Context)) {
	// ctx.Operation() ile hangi endpoint'e isabet ettiğini görebilirsiniz
	ctx.SetHeader("X-Request-ID", generateID())
	next(ctx)
})

OpenAPI şemasını özelleştirme

huma.DefaultConfig çağrısı temel bir yapılandırma döndürüyor. Bunu genişletmek kolay:

config := huma.DefaultConfig("Book API", "1.0.0")
config.Info.Description = "Kitap yönetimi için basit bir REST API"
config.Info.Contact = &huma.Contact{
	Name:  "API Destek",
	Email: "api@example.com",
}
config.Servers = []*huma.Server{
	{URL: "https://api.example.com"},
}

api := humastdlib.New(mux, config)

Üretilen OpenAPI 3.1 JSON'ı frontend ekibiyle paylaşılabilir. TypeScript tarafında openapi-typescript gibi araçlarla bu şemadan tip üretmek mümkün; böylece tRPC benzeri bir uçtan uca tip güvenliği elde edilebilir, diller arası bile olsa.

Hata yönetimi

Huma'nın hata fonksiyonları HTTP durum kodlarına karşılık geliyor:

huma.Error400BadRequest("geçersiz istek")
huma.Error404NotFound("kaynak bulunamadı")
huma.Error409Conflict("bu ID zaten mevcut")
huma.Error500InternalServerError("beklenmeyen hata")

Her biri RFC 9457 formatında yanıt üretiyor. Kendi hata tipinizi de tanımlayabilirsiniz ama çoğu durumda bu yardımcı fonksiyonlar yeterli.

Stdlib router'ın sınırları

Stdlib router'ın birkaç eksiği hâlâ var. Regex constraint yok: {id} wildcard'ı her string'i kabul ediyor, {id:[0-9]+} gibi bir kısıtlama yazılamıyor. Bu validasyonu Huma struct tag'leri ile yapıyorsunuz ama rota eşlemesi düzeyinde değil, handler öncesi validation düzeyinde gerçekleşiyor.

İkinci sınır: path parametresiz catch-all ({path...} ile yapılabiliyor ama nested gruplar, middleware scope'u gibi özellikler stdlib'de yok). Büyük bir API'de 50+ endpoint varsa ve farklı endpoint gruplarına farklı middleware uygulamak istiyorsanız, Chi'nin r.Group() yapısı daha ergonomik kalıyor. Huma'nın Chi adapter'ı da var, geçiş tek satır.

Ne zaman bu yapıyı tercih etmeli

Bağımlılık sayısını minimum tutmak istiyorsanız ve API'nizin OpenAPI dokümantasyonuna ihtiyacı varsa, stdlib + Huma ikilisi iyi bir denge noktası. Huma'nın kendisi tek bir bağımlılık ve runtime'da reflection kullanmıyor, generics tabanlı çalışıyor.

Eğer OpenAPI'ye ihtiyacınız yoksa saf stdlib yeterli. Eğer gRPC veya Connect protokolü kullanıyorsanız bu yapı uygun değil, doğrudan connectrpc paketine bakın. Hono.js gibi TypeScript framework'lerinden Go'ya geçiş yapıyorsanız, Huma'nın handler imzası ve otomatik validation yaklaşımı tanıdık gelecektir.

Kritik nokta şu: Go'nun stdlib router'ı artık basit CRUD API'ler için harici paket gerektirmiyor. Huma ise bu router'ın üzerine validation ve dokümantasyon katmanı ekliyor. İkisi birlikte, minimum bağımlılıkla production-ready bir API sunuyor.