Add examples/ with runnable usage demos for all major features
All checks were successful
CI / test (push) Successful in 31s

Six examples covering the full API surface:
- basic-client: retry, timeout, logging, response size limit
- form-request: form-encoded POST for OAuth/webhooks
- load-balancing: weighted endpoints, circuit breaker, health checks
- server-basic: routing, groups, JSON helpers, health, custom 404
- server-protected: CORS, rate limiting, body limits, timeouts
- request-id-propagation: cross-service request ID forwarding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 22:05:08 +03:00
parent 89cfc38f0e
commit 3aa7536328
7 changed files with 344 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
// Basic HTTP client with retry, timeout, and structured logging.
package main
import (
"context"
"fmt"
"log"
"log/slog"
"time"
"git.codelab.vc/pkg/httpx"
"git.codelab.vc/pkg/httpx/middleware"
"git.codelab.vc/pkg/httpx/retry"
)
func main() {
client := httpx.New(
httpx.WithBaseURL("https://httpbin.org"),
httpx.WithTimeout(10*time.Second),
httpx.WithRetry(
retry.WithMaxAttempts(3),
retry.WithBackoff(retry.ExponentialBackoff(100*time.Millisecond, 2*time.Second, true)),
),
httpx.WithMiddleware(
middleware.UserAgent("httpx-example/1.0"),
),
httpx.WithMaxResponseBody(1<<20), // 1 MB limit
httpx.WithLogger(slog.Default()),
)
defer client.Close()
ctx := context.Background()
// GET request.
resp, err := client.Get(ctx, "/get")
if err != nil {
log.Fatal(err)
}
body, err := resp.String()
if err != nil {
log.Fatal(err)
}
fmt.Printf("GET /get → %d (%d bytes)\n", resp.StatusCode, len(body))
// POST with JSON body.
type payload struct {
Name string `json:"name"`
Email string `json:"email"`
}
req, err := httpx.NewJSONRequest(ctx, "POST", "/post", payload{
Name: "Alice",
Email: "alice@example.com",
})
if err != nil {
log.Fatal(err)
}
resp, err = client.Do(ctx, req)
if err != nil {
log.Fatal(err)
}
defer resp.Close()
fmt.Printf("POST /post → %d\n", resp.StatusCode)
}

View File

@@ -0,0 +1,41 @@
// Demonstrates form-encoded requests for OAuth token endpoints and similar APIs.
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/url"
"git.codelab.vc/pkg/httpx"
)
func main() {
client := httpx.New(
httpx.WithBaseURL("https://httpbin.org"),
)
defer client.Close()
ctx := context.Background()
// Form-encoded POST, common for OAuth token endpoints.
req, err := httpx.NewFormRequest(ctx, http.MethodPost, "/post", url.Values{
"grant_type": {"client_credentials"},
"client_id": {"my-app"},
"client_secret": {"secret"},
"scope": {"read write"},
})
if err != nil {
log.Fatal(err)
}
resp, err := client.Do(ctx, req)
if err != nil {
log.Fatal(err)
}
defer resp.Close()
body, _ := resp.String()
fmt.Printf("Status: %d\nBody: %s\n", resp.StatusCode, body)
}

View File

@@ -0,0 +1,54 @@
// Demonstrates load balancing across multiple backend endpoints with
// circuit breaking and health-checked failover.
package main
import (
"context"
"fmt"
"log"
"log/slog"
"time"
"git.codelab.vc/pkg/httpx"
"git.codelab.vc/pkg/httpx/balancer"
"git.codelab.vc/pkg/httpx/circuitbreaker"
"git.codelab.vc/pkg/httpx/retry"
)
func main() {
client := httpx.New(
httpx.WithEndpoints(
balancer.Endpoint{URL: "http://localhost:8081", Weight: 3},
balancer.Endpoint{URL: "http://localhost:8082", Weight: 1},
),
httpx.WithBalancer(
balancer.WithStrategy(balancer.WeightedRandom()),
balancer.WithHealthCheck(
balancer.WithHealthInterval(5*time.Second),
balancer.WithHealthPath("/healthz"),
),
),
httpx.WithCircuitBreaker(
circuitbreaker.WithFailureThreshold(5),
circuitbreaker.WithOpenDuration(30*time.Second),
),
httpx.WithRetry(
retry.WithMaxAttempts(3),
),
httpx.WithLogger(slog.Default()),
)
defer client.Close()
ctx := context.Background()
for i := range 5 {
resp, err := client.Get(ctx, fmt.Sprintf("/api/item/%d", i))
if err != nil {
log.Printf("request %d failed: %v", i, err)
continue
}
body, _ := resp.String()
fmt.Printf("request %d → %d: %s\n", i, resp.StatusCode, body)
}
}

View File

@@ -0,0 +1,54 @@
// Demonstrates request ID propagation between server and client.
// The server assigns a request ID to incoming requests, and the client
// middleware forwards it to downstream services via X-Request-Id header.
package main
import (
"fmt"
"log"
"log/slog"
"net/http"
"git.codelab.vc/pkg/httpx"
"git.codelab.vc/pkg/httpx/middleware"
"git.codelab.vc/pkg/httpx/server"
)
func main() {
logger := slog.Default()
// Client that propagates request IDs from context.
client := httpx.New(
httpx.WithBaseURL("http://localhost:9090"),
httpx.WithMiddleware(
middleware.RequestID(), // Picks up ID from context, sets X-Request-Id.
),
)
defer client.Close()
r := server.NewRouter()
r.HandleFunc("GET /proxy", func(w http.ResponseWriter, r *http.Request) {
// The request ID is in r.Context() thanks to server.RequestID().
id := server.RequestIDFromContext(r.Context())
logger.Info("handling request", "request_id", id)
// Client automatically forwards the request ID to downstream.
resp, err := client.Get(r.Context(), "/downstream")
if err != nil {
server.WriteError(w, http.StatusBadGateway, fmt.Sprintf("downstream error: %v", err))
return
}
defer resp.Close()
body, _ := resp.String()
server.WriteJSON(w, http.StatusOK, map[string]string{
"request_id": id,
"downstream_response": body,
})
})
// Server with RequestID middleware that assigns IDs to incoming requests.
srv := server.New(r, server.Defaults(logger)...)
log.Fatal(srv.ListenAndServe())
}

View File

@@ -0,0 +1,54 @@
// Basic HTTP server with routing, middleware, health checks, and graceful shutdown.
package main
import (
"log"
"log/slog"
"net/http"
"git.codelab.vc/pkg/httpx/server"
)
func main() {
logger := slog.Default()
r := server.NewRouter(
server.WithNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
server.WriteError(w, http.StatusNotFound, "resource not found")
})),
)
// Public endpoints.
r.HandleFunc("GET /hello", func(w http.ResponseWriter, _ *http.Request) {
server.WriteJSON(w, http.StatusOK, map[string]string{
"message": "Hello, World!",
})
})
// API group with shared prefix.
api := r.Group("/api/v1")
api.HandleFunc("GET /users/{id}", getUser)
api.HandleFunc("POST /users", createUser)
// Health checks.
r.Mount("/", server.HealthHandler())
// Server with production defaults (RequestID → Recovery → Logging).
srv := server.New(r, server.Defaults(logger)...)
log.Fatal(srv.ListenAndServe())
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
server.WriteJSON(w, http.StatusOK, map[string]string{
"id": id,
"name": "Alice",
})
}
func createUser(w http.ResponseWriter, r *http.Request) {
server.WriteJSON(w, http.StatusCreated, map[string]any{
"id": 1,
"message": "user created",
})
}

View File

@@ -0,0 +1,61 @@
// Production server with CORS, rate limiting, body size limits, and timeouts.
package main
import (
"log"
"log/slog"
"net/http"
"time"
"git.codelab.vc/pkg/httpx/server"
)
func main() {
logger := slog.Default()
r := server.NewRouter(
server.WithNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
server.WriteError(w, http.StatusNotFound, "not found")
})),
)
r.HandleFunc("GET /api/data", func(w http.ResponseWriter, _ *http.Request) {
server.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
r.HandleFunc("POST /api/upload", func(w http.ResponseWriter, r *http.Request) {
// Body is already limited by MaxBodySize middleware.
server.WriteJSON(w, http.StatusAccepted, map[string]string{"status": "received"})
})
r.Mount("/", server.HealthHandler())
srv := server.New(r,
append(
server.Defaults(logger),
server.WithMiddleware(
// CORS for browser-facing APIs.
server.CORS(
server.AllowOrigins("https://app.example.com", "https://admin.example.com"),
server.AllowMethods("GET", "POST", "PUT", "PATCH", "DELETE"),
server.AllowHeaders("Authorization", "Content-Type"),
server.ExposeHeaders("X-Request-Id"),
server.AllowCredentials(true),
server.MaxAge(3600),
),
// Rate limit: 100 req/s per IP, burst of 200.
server.RateLimit(
server.WithRate(100),
server.WithBurst(200),
),
// Limit request body to 1 MB.
server.MaxBodySize(1<<20),
// Per-request timeout of 30 seconds.
server.Timeout(30*time.Second),
),
server.WithAddr(":8080"),
)...,
)
log.Fatal(srv.ListenAndServe())
}