go / http clients
I prefer a small client of my own to a vendor SDK. I made the same argument for Ruby.
The standard library net/http covers most of what an SDK wraps:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
func main() {
url := os.Getenv("API_URL")
if url == "" {
log.Fatalln("err: API_URL environment variable is not set")
}
reqBody, err := json.Marshal(map[string]string{"text": "hi"})
if err != nil {
log.Fatalf("err: %v\n", err)
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer(reqBody))
if err != nil {
log.Fatalf("err: %v\n", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("err: %v\n", err)
}
fmt.Println(string(body))
}
SDK costs
An SDK needs upgrades for security patches and dependency conflicts. It can become unmaintained. It is one more interface to learn.
A replacement covers only the endpoints the app calls. Most of an SDK is code the app never uses.
Shared shape
Each client wraps only the endpoints I use. All share one helper for retries, so a client is mostly request building and response mapping:
res, err := httputil.Do(ctx, func() (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
return req, nil
}, httputil.Config{
Client: c.http,
RetryDelays: retryDelays,
TransientCodes: transientCodes,
})
httputil.Do calls the build closure once per attempt. The retry
schedule is a lookup table. Each client picks its own
transient codes and delays.
Tests point the HTTP client at an httptest.NewServer. Higher layers
take one-method interfaces such as Uploader and inject fakes.
Clients
Clients built this way:
- cloudflare: retries on idempotent uploads, an SSRF guard, and content-type detection from bytes.
- render: polls a deploy API for cmd/deploy.
- s3: AWS SigV4 request signing and presigned URLs.
- slack:
chat.postMessage, per-method retry policy, and200 {"ok": false}errors. - workos: OAuth with CSRF state and a consent step.
- sentry: error capture without tracing or breadcrumbs.
Libraries I do use
A narrow, audited library for one job, such as JWT verification, is a fair trade. I avoid the vendor SDK that pulls a large dependency tree to wrap endpoints I can call in a dozen lines.