From 93b6c36e49bb7226f105a8f7f7e12a82ceb7d663 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 29 Dec 2025 20:41:02 -0600 Subject: [PATCH 1/9] docs: cleanup --- README.md | 77 +++++++++++++++++++++++++++++++++++++++------ client.go | 1 + examples/do/main.go | 1 + 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 56f84fc..e23cc0a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ package main import ( "context" "fmt" + "log" "net/http" "github.com/goforj/httpx" @@ -54,22 +55,26 @@ import ( ) func main() { - c := httpx.New() + type GetResponse struct { + URL string `json:"url"` + } + + c := httpx.New(httpx.UserAgent("demo/1.0")) // Simple typed GET. - res, err := httpx.Get[map[string]any](c, "https://httpbin.org/get") + res, err := httpx.Get[GetResponse](c, "https://httpbin.org/get") if err != nil { - panic(err) + log.Fatal(err) } - fmt.Println(res) + fmt.Println(res.URL) // Context-aware GET. ctx := context.Background() - res2, err := httpx.GetCtx[map[string]any](c, ctx, "https://httpbin.org/get") + res2, err := httpx.GetCtx[GetResponse](c, ctx, "https://httpbin.org/get") if err != nil { - panic(err) + log.Fatal(err) } - fmt.Println(res2) + fmt.Println(res2.URL) // Access the underlying response when you need it. r := req.C().R() @@ -77,11 +82,64 @@ func main() { r.Method = http.MethodGet res3, resp, err := httpx.Do[map[string]any](r) if err != nil { - panic(err) + log.Fatal(err) } fmt.Println(res3) - fmt.Println(resp.Status) + fmt.Println(resp.StatusCode) +} +``` + +## Most Common Tasks + +Brevity-first, idiomatic patterns. Handle errors; dump what matters. + +```go +type GetResponse struct { + URL string `json:"url"` +} + +c := httpx.New() + +// Typed GET +res, err := httpx.Get[GetResponse](c, "https://httpbin.org/get") +if err != nil { + // handler error +} +httpx.Dump(res) // URL => "https://httpbin.org/get" + +// POST JSON (typed request/response) +type CreateUser struct { + Name string `json:"name"` +} +type CreateUserResponse struct { + JSON CreateUser `json:"json"` +} +resPost, err := httpx.Post[CreateUser, CreateUserResponse](c, "https://httpbin.org/post", CreateUser{Name: "Ana"}) +if err != nil { + panic(err) +} +httpx.Dump(resPost) // JSON.Name => "Ana" + +// Headers +res, err = httpx.Get[map[string]any](c, "https://httpbin.org/headers", httpx.Header("X-Test", "true")) +if err != nil { + panic(err) +} +httpx.Dump(res) // headers.X-Test => "true" + +// Query params +res, err = httpx.Get[map[string]any](c, "https://httpbin.org/get", httpx.Query("q", "search")) +if err != nil { + panic(err) +} +httpx.Dump(res) // args.q => "search" + +// File upload +resUpload, err := httpx.Post[any, map[string]any](c, "https://httpbin.org/post", nil, httpx.File("file", "./report.txt")) +if err != nil { + panic(err) } +httpx.Dump(resUpload) // files.file => "...report.txt" ``` ## v2 Error Handling @@ -890,6 +948,7 @@ httpx.Dump(res) // dumps DeleteResponse ### Do Do executes a pre-configured req request and returns the decoded body and response. +This is the low-level escape hatch when you need full req control. ```go r := req.C().R().SetHeader("X-Trace", "1") diff --git a/client.go b/client.go index 79c1e72..b389e90 100644 --- a/client.go +++ b/client.go @@ -447,6 +447,7 @@ func OptionsCtx[T any](client *Client, ctx context.Context, url string, opts ... } // Do executes a pre-configured req request and returns the decoded body and response. +// This is the low-level escape hatch when you need full req control. // @group Requests // // Example: advanced request with response access diff --git a/examples/do/main.go b/examples/do/main.go index 6885aac..33663b3 100644 --- a/examples/do/main.go +++ b/examples/do/main.go @@ -11,6 +11,7 @@ import ( func main() { // Do executes a pre-configured req request and returns the decoded body and response. + // This is the low-level escape hatch when you need full req control. // Example: advanced request with response access r := req.C().R().SetHeader("X-Trace", "1") From 31b6c46bc3389b9edc5ed5110ab8b49b82e73829 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 29 Dec 2025 20:48:39 -0600 Subject: [PATCH 2/9] docs: cleanup --- README.md | 124 ++++++++++++++++++------------------------------------ 1 file changed, 42 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index e23cc0a..5bd609a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,17 @@ It keeps req's power and escape hatches, while making the 90% use case feel effo httpx v1 has been tagged and is now frozen. The `main` branch is v2, which includes intentional breaking changes to improve API clarity and ergonomics (for example, request helpers return `(T, error)`). +## The 90% You Need + +Learn these first—most users never need more: + +1. `Get / Post / Put / Patch / Delete` (+ `*Ctx` variants) +2. `Query`, `Path`, `Header`, `JSON` +3. `New(...)` client options (BaseURL, auth, timeouts) +4. `Retry*` (when you need resiliency) +5. `Dump` (quick debugging) +6. `Req`/`Raw` escape hatch when you outgrow the helpers + ## Why httpx - Typed, zero-ceremony requests with generics. @@ -42,118 +53,67 @@ go get github.com/goforj/httpx ## Quick Start ```go -package main - -import ( - "context" - "fmt" - "log" - "net/http" +type User struct { + ID int `json:"id"` + Name string `json:"name"` +} - "github.com/goforj/httpx" - "github.com/imroc/req/v3" +c := httpx.New( + httpx.BaseURL("https://api.example.com"), + httpx.Bearer("token"), ) -func main() { - type GetResponse struct { - URL string `json:"url"` - } - - c := httpx.New(httpx.UserAgent("demo/1.0")) - - // Simple typed GET. - res, err := httpx.Get[GetResponse](c, "https://httpbin.org/get") - if err != nil { - log.Fatal(err) - } - fmt.Println(res.URL) - - // Context-aware GET. - ctx := context.Background() - res2, err := httpx.GetCtx[GetResponse](c, ctx, "https://httpbin.org/get") - if err != nil { - log.Fatal(err) - } - fmt.Println(res2.URL) - - // Access the underlying response when you need it. - r := req.C().R() - r.SetURL("https://httpbin.org/get") - r.Method = http.MethodGet - res3, resp, err := httpx.Do[map[string]any](r) - if err != nil { - log.Fatal(err) - } - fmt.Println(res3) - fmt.Println(resp.StatusCode) +user, err := httpx.Get[User](c, "/users/42", httpx.Query("include", "profile")) +if err != nil { + log.Fatal(err) } + +fmt.Println(user.Name) ``` +All request helpers return `(T, error)`. The low-level escape hatch `Do` returns `(T, *req.Response, error)` when you need raw access. + ## Most Common Tasks Brevity-first, idiomatic patterns. Handle errors; dump what matters. +Singular helpers (`Query`, `Path`, `Header`) set one value; plural helpers (`Queries`, `Paths`, `Headers`) set many at once. + ```go type GetResponse struct { URL string `json:"url"` } - -c := httpx.New() - -// Typed GET -res, err := httpx.Get[GetResponse](c, "https://httpbin.org/get") -if err != nil { - // handler error -} -httpx.Dump(res) // URL => "https://httpbin.org/get" - -// POST JSON (typed request/response) type CreateUser struct { Name string `json:"name"` } type CreateUserResponse struct { JSON CreateUser `json:"json"` } -resPost, err := httpx.Post[CreateUser, CreateUserResponse](c, "https://httpbin.org/post", CreateUser{Name: "Ana"}) -if err != nil { - panic(err) + +c := httpx.New() +must := func[T any](v T, err error) T { + if err != nil { + log.Fatal(err) + } + return v } + +res := must(httpx.Get[GetResponse](c, "https://httpbin.org/get")) +httpx.Dump(res) // URL => "https://httpbin.org/get" + +resPost := must(httpx.Post[CreateUser, CreateUserResponse](c, "https://httpbin.org/post", CreateUser{Name: "Ana"})) httpx.Dump(resPost) // JSON.Name => "Ana" -// Headers -res, err = httpx.Get[map[string]any](c, "https://httpbin.org/headers", httpx.Header("X-Test", "true")) -if err != nil { - panic(err) -} +res = must(httpx.Get[map[string]any](c, "https://httpbin.org/headers", httpx.Header("X-Test", "true"))) httpx.Dump(res) // headers.X-Test => "true" -// Query params -res, err = httpx.Get[map[string]any](c, "https://httpbin.org/get", httpx.Query("q", "search")) -if err != nil { - panic(err) -} +res = must(httpx.Get[map[string]any](c, "https://httpbin.org/get", httpx.Query("q", "search"))) httpx.Dump(res) // args.q => "search" -// File upload -resUpload, err := httpx.Post[any, map[string]any](c, "https://httpbin.org/post", nil, httpx.File("file", "./report.txt")) -if err != nil { - panic(err) -} +resUpload := must(httpx.Post[any, map[string]any](c, "https://httpbin.org/post", nil, httpx.File("file", "./report.txt"))) httpx.Dump(resUpload) // files.file => "...report.txt" ``` -## v2 Error Handling - -httpx v2 returns errors as the second return value from request helpers. - -```go -res, err := httpx.Get[map[string]any](c, "https://httpbin.org/get") -if err != nil { - return err -} -_ = res -``` - ## Browser Profiles Browser profiles provide a simple way to match common client behavior without exposing low-level details. From f1e0981a94cd0e5c7e3a15a8a0659fb9482a29f5 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 29 Dec 2025 20:55:29 -0600 Subject: [PATCH 3/9] docs: cleanup --- README.md | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 5bd609a..9eb3a49 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,6 @@ It keeps req's power and escape hatches, while making the 90% use case feel effo httpx v1 has been tagged and is now frozen. The `main` branch is v2, which includes intentional breaking changes to improve API clarity and ergonomics (for example, request helpers return `(T, error)`). -## The 90% You Need - -Learn these first—most users never need more: - -1. `Get / Post / Put / Patch / Delete` (+ `*Ctx` variants) -2. `Query`, `Path`, `Header`, `JSON` -3. `New(...)` client options (BaseURL, auth, timeouts) -4. `Retry*` (when you need resiliency) -5. `Dump` (quick debugging) -6. `Req`/`Raw` escape hatch when you outgrow the helpers - ## Why httpx - Typed, zero-ceremony requests with generics. @@ -64,15 +53,22 @@ c := httpx.New( ) user, err := httpx.Get[User](c, "/users/42", httpx.Query("include", "profile")) -if err != nil { - log.Fatal(err) -} +_ = err fmt.Println(user.Name) ``` All request helpers return `(T, error)`. The low-level escape hatch `Do` returns `(T, *req.Response, error)` when you need raw access. +## Core Surface (use these first) + +- Requests: `Get / Post / Put / Patch / Delete` (+ `*Ctx`) +- Composition: `Query`, `Path`, `Header`, `JSON` +- Client defaults: `New(...)` with BaseURL, auth, timeouts +- Resiliency: `Retry*` +- Debug: `Dump` +- Power-user: `Req` / `Raw` escape hatch + ## Most Common Tasks Brevity-first, idiomatic patterns. Handle errors; dump what matters. @@ -91,26 +87,20 @@ type CreateUserResponse struct { } c := httpx.New() -must := func[T any](v T, err error) T { - if err != nil { - log.Fatal(err) - } - return v -} -res := must(httpx.Get[GetResponse](c, "https://httpbin.org/get")) +res, _ := httpx.Get[GetResponse](c, "https://httpbin.org/get") httpx.Dump(res) // URL => "https://httpbin.org/get" -resPost := must(httpx.Post[CreateUser, CreateUserResponse](c, "https://httpbin.org/post", CreateUser{Name: "Ana"})) +resPost, _ := httpx.Post[CreateUser, CreateUserResponse](c, "https://httpbin.org/post", CreateUser{Name: "Ana"}) httpx.Dump(resPost) // JSON.Name => "Ana" -res = must(httpx.Get[map[string]any](c, "https://httpbin.org/headers", httpx.Header("X-Test", "true"))) +res, _ = httpx.Get[map[string]any](c, "https://httpbin.org/headers", httpx.Header("X-Test", "true")) httpx.Dump(res) // headers.X-Test => "true" -res = must(httpx.Get[map[string]any](c, "https://httpbin.org/get", httpx.Query("q", "search"))) +res, _ = httpx.Get[map[string]any](c, "https://httpbin.org/get", httpx.Query("q", "search")) httpx.Dump(res) // args.q => "search" -resUpload := must(httpx.Post[any, map[string]any](c, "https://httpbin.org/post", nil, httpx.File("file", "./report.txt"))) +resUpload, _ := httpx.Post[any, map[string]any](c, "https://httpbin.org/post", nil, httpx.File("file", "./report.txt")) httpx.Dump(resUpload) // files.file => "...report.txt" ``` From 01491436380b0d5e5fbf2092a615d5d788248fb2 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 29 Dec 2025 21:13:04 -0600 Subject: [PATCH 4/9] docs: cleanup --- README.md | 17 ++++++++--------- client.go | 12 +++--------- examples/get/main.go | 12 +++--------- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 9eb3a49..9d5792c 100644 --- a/README.md +++ b/README.md @@ -88,18 +88,23 @@ type CreateUserResponse struct { c := httpx.New() +// Simple request res, _ := httpx.Get[GetResponse](c, "https://httpbin.org/get") httpx.Dump(res) // URL => "https://httpbin.org/get" +// POST JSON with typed request/response resPost, _ := httpx.Post[CreateUser, CreateUserResponse](c, "https://httpbin.org/post", CreateUser{Name: "Ana"}) httpx.Dump(resPost) // JSON.Name => "Ana" +// Passing a header option res, _ = httpx.Get[map[string]any](c, "https://httpbin.org/headers", httpx.Header("X-Test", "true")) httpx.Dump(res) // headers.X-Test => "true" +// Passing query params res, _ = httpx.Get[map[string]any](c, "https://httpbin.org/get", httpx.Query("q", "search")) httpx.Dump(res) // args.q => "search" +// File upload resUpload, _ := httpx.Post[any, map[string]any](c, "https://httpbin.org/post", nil, httpx.File("file", "./report.txt")) httpx.Dump(resUpload) // files.file => "...report.txt" ``` @@ -930,10 +935,7 @@ type GetResponse struct { } c := httpx.New() -res, err := httpx.Get[GetResponse](c, "https://httpbin.org/get") -if err != nil { - return -} +res, _ := httpx.Get[GetResponse](c, "https://httpbin.org/get") httpx.Dump(res) // #GetResponse { // URL => "https://httpbin.org/get" #string @@ -943,11 +945,8 @@ httpx.Dump(res) _Example: bind to a string body_ ```go -resText, err := httpx.Get[string](c, "https://httpbin.org/uuid") -if err != nil { - return -} -println(resText) // dumps string +resString, _ := httpx.Get[string](c, "https://httpbin.org/uuid") +println(resString) // dumps string // { // "uuid": "becbda6d-9950-4966-ae23-0369617ba065" // } diff --git a/client.go b/client.go index b389e90..b4d5400 100644 --- a/client.go +++ b/client.go @@ -118,10 +118,7 @@ func (c *Client) clone() *Client { // } // // c := httpx.New() -// res, err := httpx.Get[GetResponse](c, "https://httpbin.org/get") -// if err != nil { -// return -// } +// res, _ := httpx.Get[GetResponse](c, "https://httpbin.org/get") // httpx.Dump(res) // // #GetResponse { // // URL => "https://httpbin.org/get" #string @@ -129,11 +126,8 @@ func (c *Client) clone() *Client { // // Example: bind to a string body // -// resText, err := httpx.Get[string](c, "https://httpbin.org/uuid") -// if err != nil { -// return -// } -// println(resText) // dumps string +// resString, _ := httpx.Get[string](c, "https://httpbin.org/uuid") +// println(resString) // dumps string // // { // // "uuid": "becbda6d-9950-4966-ae23-0369617ba065" // // } diff --git a/examples/get/main.go b/examples/get/main.go index 1484d59..43d8e0c 100644 --- a/examples/get/main.go +++ b/examples/get/main.go @@ -14,21 +14,15 @@ func main() { } c := httpx.New() - res, err := httpx.Get[GetResponse](c, "https://httpbin.org/get") - if err != nil { - return - } + res, _ := httpx.Get[GetResponse](c, "https://httpbin.org/get") httpx.Dump(res) // #GetResponse { // URL => "https://httpbin.org/get" #string // } // Example: bind to a string body - resText, err := httpx.Get[string](c, "https://httpbin.org/uuid") - if err != nil { - return - } - println(resText) // dumps string + resString, _ := httpx.Get[string](c, "https://httpbin.org/uuid") + println(resString) // dumps string // { // "uuid": "becbda6d-9950-4966-ae23-0369617ba065" // } From d8088317021fb2190bdaac595f5e898efa4470df Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 29 Dec 2025 21:16:19 -0600 Subject: [PATCH 5/9] docs: cleanup --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9d5792c..74ad305 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,10 @@ It keeps req's power and escape hatches, while making the 90% use case feel effo httpx Logo

-## v2 Status - -httpx v1 has been tagged and is now frozen. The `main` branch is v2, which includes intentional breaking changes to improve API clarity and ergonomics (for example, request helpers return `(T, error)`). - ## Why httpx +- Developer-first ergonomics: fast to read, fast to write, predictable to use. +- Reliability by default: sensible timeouts, safe error handling, resilient retries. - Typed, zero-ceremony requests with generics. - Opinionated defaults (timeouts, result handling, safe error mapping). - Built on req, with full escape hatches via `Client.Req()` and `Client.Raw()`. @@ -175,6 +173,10 @@ They are compiled by `example_compile_test.go` to keep docs and code in sync. - Run `go run ./docs/readme/main.go` to refresh the API index and test count. - Run `go test ./...`. +## v2 Status + +httpx v1 has been tagged and is now frozen. The `main` branch is v2, which includes intentional breaking changes to improve API clarity and ergonomics (for example, request helpers return `(T, error)`). + ## API Index From f8df0aca2a3dbef22cc6d353c5f03bce5390d88c Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 29 Dec 2025 21:17:39 -0600 Subject: [PATCH 6/9] docs: cleanup --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 74ad305..2246752 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,11 @@ type User struct { c := httpx.New( httpx.BaseURL("https://api.example.com"), httpx.Bearer("token"), + // options... ) -user, err := httpx.Get[User](c, "/users/42", httpx.Query("include", "profile")) -_ = err - +// httpx.Get[T](client, url, options...) +user, _ := httpx.Get[User](c, "/users/42", httpx.Query("include", "profile")) fmt.Println(user.Name) ``` From be315670cea5d6388152b02f517ab971477a60b8 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 29 Dec 2025 21:26:10 -0600 Subject: [PATCH 7/9] docs: cleanup --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2246752..bb98b14 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,12 @@ res, err := httpx.Get[map[string]any]( httpx.Path("id", "42"), httpx.Query("include", "teams", "active", "1"), httpx.Header("Accept", "application/json"), + // stack more options... ) if err != nil { - panic(err) + // handler error } -_ = res +httpx.Dump(res) // dumps map[string]any ``` ## Debugging and Tracing From 14b8a60127ecaf6a21b88cba00e89eaf0ea0dbbf Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 29 Dec 2025 21:26:44 -0600 Subject: [PATCH 8/9] docs: cleanup --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb98b14..95ae1bc 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ res, err := httpx.Get[map[string]any]( // stack more options... ) if err != nil { - // handler error + // handle error } httpx.Dump(res) // dumps map[string]any ``` From 7df697242634310b9917a270de31ab06f6ebb040 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Mon, 29 Dec 2025 21:30:52 -0600 Subject: [PATCH 9/9] docs: tweaks --- README.md | 2 +- examples/json/main.go | 2 +- options_request.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 95ae1bc..2aaf020 100644 --- a/README.md +++ b/README.md @@ -738,7 +738,7 @@ httpx.Dump(res) // dumps map[string]any ### JSON -JSON sets the request body as JSON. +JSON forces JSON encoding for the request body (even when inference might choose differently). ```go type Payload struct { diff --git a/examples/json/main.go b/examples/json/main.go index bc278d6..2a28b5a 100644 --- a/examples/json/main.go +++ b/examples/json/main.go @@ -6,7 +6,7 @@ package main import "github.com/goforj/httpx" func main() { - // JSON sets the request body as JSON. + // JSON forces JSON encoding for the request body (even when inference might choose differently). // Example: force JSON body type Payload struct { diff --git a/options_request.go b/options_request.go index 506d5f1..8206bbb 100644 --- a/options_request.go +++ b/options_request.go @@ -278,7 +278,7 @@ func (b OptionBuilder) Body(value any) OptionBuilder { })) } -// JSON sets the request body as JSON. +// JSON forces JSON encoding for the request body (even when inference might choose differently). // @group Request Composition // // Applies to individual requests only.