A fluent, batteries-included HTTP client wrapper for Go that makes common patterns trivial while staying close to Go's standard library philosophy.
- 🔗 Fluent & Chainable - Easy to read and write API
- ⚡ Sensible Defaults - Works great out of the box
- 🎯 Gradual Complexity - Simple things simple, complex things possible
- 🛡️ Production Ready - Retries, timeouts, circuit breakers included
- 🧪 Testable - Easy mocking and request recording
- 📝 Well Documented - Comprehensive examples and documentation
go get github.com/jnaneshd/httpcresp, err := httpc.New().
Get("https://api.example.com/users").
Do()
if err != nil {
log.Fatal(err)
}
defer resp.Close()
fmt.Println(resp.Status())user := User{Name: "John", Email: "john@example.com"}
resp, err := httpc.New().
Post("https://api.example.com/users").
JSON(user).
Do()// Get response as string - perfect for printing JSON
jsonStr, err := httpc.New().
Get("https://api.example.com/users").
String()
fmt.Println(jsonStr) // {"id": 1, "name": "John", ...}
// Or as bytes
data, err := httpc.New().
Get("https://api.example.com/users").
Bytes()var users []User
err := httpc.New().
Get("https://api.example.com/users").
DecodeJSON(&users)resp, err := httpc.New().
Get("https://api.example.com/users").
Header("Authorization", "Bearer token").
Query("page", "1").
Query("limit", "10").
Do()client := httpc.New().
WithBaseURL("https://api.example.com").
WithHeader("User-Agent", "MyApp/1.0").
WithHeader("Accept", "application/json")
// Now all requests use the base URL
resp, err := client.Get("/users").Do() // GET https://api.example.com/usersclient := httpc.New().
WithTimeout(30 * time.Second)
// Or per-request timeout
resp, err := client.Get("/slow-endpoint").
Timeout(60 * time.Second).
Do()client := httpc.New().
WithRetry(3, time.Second) // 3 attempts, 1s initial delay
// Or with custom configuration
client := httpc.New().
WithRetryConfig(&httpc.RetryConfig{
MaxAttempts: 5,
InitialDelay: 500 * time.Millisecond,
MaxDelay: 30 * time.Second,
Multiplier: 2.0, // Exponential backoff
Jitter: true, // Randomize delays
})Prevent cascading failures by stopping requests to failing services:
client := httpc.New().
WithCircuitBreaker(5, time.Minute) // Open after 5 failures, try again after 1 min
// Or with custom configuration
client := httpc.New().
WithCircuitBreakerConfig(&httpc.CircuitBreakerConfig{
Name: "my-service",
MaxFailures: 5,
Timeout: 60 * time.Second,
OnStateChange: func(name string, from, to gobreaker.State) {
log.Printf("Circuit breaker %s: %s -> %s", name, from, to)
},
})client := httpc.New().
WithRateLimiter(10) // 10 requests per second
// Or with burst capacity
client := httpc.New().
WithRateLimiterConfig(10, 20) // 10 req/s with burst of 20Add cross-cutting concerns like authentication, logging, or metrics:
import "net/http"
// Authentication middleware
authMiddleware := func(req *http.Request, next httpc.HTTPHandler) (*httpc.Response, error) {
req.Header.Set("Authorization", "Bearer " + getToken())
return next(req)
}
// Logging middleware
loggingMiddleware := func(req *http.Request, next httpc.HTTPHandler) (*httpc.Response, error) {
start := time.Now()
resp, err := next(req)
log.Printf("%s %s took %v", req.Method, req.URL, time.Since(start))
return resp, err
}
client := httpc.New().
WithMiddleware(authMiddleware).
WithMiddleware(loggingMiddleware)// Bearer token
client.WithMiddleware(httpc.BearerTokenMiddleware("your-token"))
// Dynamic token (refreshed on each request)
client.WithMiddleware(httpc.DynamicBearerTokenMiddleware(func() (string, error) {
return refreshToken()
}))
// User-Agent
client.WithMiddleware(httpc.UserAgentMiddleware("MyApp/1.0"))
// Default headers
client.WithMiddleware(httpc.HeadersMiddleware(map[string]string{
"X-API-Key": "secret",
}))// JSON
client.Post("/users").JSON(user)
// Form data
client.Post("/login").Form(map[string]string{
"username": "john",
"password": "secret",
})
// Raw bytes
client.Post("/upload").BodyBytes(data)
// String
client.Post("/text").BodyString("Hello, World!")
// Reader
client.Post("/stream").Body(reader)// Bearer token
client.Get("/protected").Bearer("your-token")
// Basic auth
client.Get("/protected").BasicAuth("username", "password")ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.Get("/users").Context(ctx).Do()// Returns error if status doesn't match
resp, err := client.Post("/users").
JSON(user).
ExpectStatus(201).
Do()These methods execute the request and return the result directly:
// Get raw JSON string - no decoding needed
jsonStr, err := client.Get("/users").String()
fmt.Println(jsonStr) // prints raw JSON
// Get raw bytes
data, err := client.Get("/file").Bytes()
// Decode JSON directly
var users []User
err := client.Get("/users").DecodeJSON(&users)For more control, use .Do() to get the full response:
resp, err := client.Get("/users").Do()
if err != nil {
log.Fatal(err)
}
defer resp.Close()
// Status
fmt.Println(resp.StatusCode()) // 200
fmt.Println(resp.Status()) // "200 OK"
fmt.Println(resp.IsSuccess()) // true (2xx)
fmt.Println(resp.IsError()) // false (4xx or 5xx)
fmt.Println(resp.IsClientError()) // false (4xx)
fmt.Println(resp.IsServerError()) // false (5xx)
// Headers
fmt.Println(resp.Header("Content-Type"))
fmt.Println(resp.ContentType())
fmt.Println(resp.ContentLength())
// Body (multiple ways)
body, _ := resp.String() // As string (for printing/logging)
bytes, _ := resp.Bytes() // As []byte (for binary data)
var data MyStruct
resp.JSON(&data) // Decode JSON into struct
// Cookies
cookies := resp.Cookies()import "github.com/jnaneshd/httpc/mock"
func TestAPI(t *testing.T) {
server := mock.NewServer()
defer server.Close()
// Set up mock response
server.OnGet("/users/1").
Status(200).
JSON(User{ID: 1, Name: "John"}).
Reply()
// Use mock server URL
client := httpc.New().WithBaseURL(server.URL())
var user User
err := client.Get("/users/1").DecodeJSON(&user)
if err != nil {
t.Fatal(err)
}
// Verify request was made
if server.RequestCount() != 1 {
t.Errorf("expected 1 request, got %d", server.RequestCount())
}
}server.OnPost("/users").
Handler(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Header.Get("Authorization") == "" {
w.WriteHeader(401)
return
}
w.WriteHeader(201)
w.Write([]byte(`{"id": 1}`))
}).
Reply()// Access recorded requests
requests := server.Requests()
lastReq := server.LastRequest()
fmt.Println(lastReq.Method) // "POST"
fmt.Println(lastReq.Path) // "/users"
fmt.Println(lastReq.Headers) // http.Headerpackage main
import (
"fmt"
"log"
"time"
"github.com/jnaneshd/httpc"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
// Create a production-ready client
client := httpc.New().
WithBaseURL("https://api.example.com").
WithHeader("User-Agent", "MyApp/1.0").
WithTimeout(30 * time.Second).
WithRetry(3, time.Second).
WithCircuitBreaker(5, time.Minute).
WithRateLimiter(100).
WithMiddleware(httpc.BearerTokenMiddleware(getToken()))
// Create a user
newUser := User{Name: "John", Email: "john@example.com"}
var createdUser User
err := client.
Post("/users").
JSON(newUser).
ExpectStatus(201).
DecodeJSON(&createdUser)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created user: %+v\n", createdUser)
// Get all users
var users []User
err = client.
Get("/users").
Query("limit", "10").
DecodeJSON(&users)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d users\n", len(users))
}
func getToken() string {
return "your-api-token"
}resp, err := client.Get("/users").Do()
if err != nil {
var httpErr *httpc.Error
if errors.As(err, &httpErr) {
fmt.Println("Operation:", httpErr.Op)
fmt.Println("URL:", httpErr.URL)
fmt.Println("Status:", httpErr.StatusCode)
fmt.Println("Retryable:", httpErr.IsRetryable())
}
}if errors.Is(err, httpc.ErrCircuitOpen) {
// Circuit breaker is open
}
if errors.Is(err, httpc.ErrRateLimited) {
// Rate limit exceeded
}
if errors.Is(err, httpc.ErrRetryExhausted) {
// All retry attempts failed
}| Feature | httpc | net/http | resty |
|---|---|---|---|
| Fluent API | ✅ | ❌ | ✅ |
| JSON helpers | ✅ | ❌ | ✅ |
| Retries | ✅ | ❌ | ✅ |
| Circuit breaker | ✅ | ❌ | ❌ |
| Rate limiting | ✅ | ❌ | ❌ |
| Mock server | ✅ | ❌ | ❌ |
| Middleware | ✅ | ✅ | ✅ |
| Lightweight | ✅ | ✅ | ❌ |
- Go 1.21 or later
- golang.org/x/time/rate - Rate limiting
- github.com/sony/gobreaker - Circuit breaker
MIT License - see LICENSE for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request