diff --git a/README.md b/README.md index 56f84fc..2aaf020 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()`. @@ -42,58 +40,71 @@ go get github.com/goforj/httpx ## Quick Start ```go -package main - -import ( - "context" - "fmt" - "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"), + // options... ) -func main() { - c := httpx.New() - - // Simple typed GET. - res, err := httpx.Get[map[string]any](c, "https://httpbin.org/get") - if err != nil { - panic(err) - } - fmt.Println(res) - - // Context-aware GET. - ctx := context.Background() - res2, err := httpx.GetCtx[map[string]any](c, ctx, "https://httpbin.org/get") - if err != nil { - panic(err) - } - fmt.Println(res2) - - // 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 { - panic(err) - } - fmt.Println(res3) - fmt.Println(resp.Status) -} +// httpx.Get[T](client, url, options...) +user, _ := httpx.Get[User](c, "/users/42", httpx.Query("include", "profile")) +fmt.Println(user.Name) ``` -## v2 Error Handling +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) -httpx v2 returns errors as the second return value from request helpers. +- 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. + +Singular helpers (`Query`, `Path`, `Header`) set one value; plural helpers (`Queries`, `Paths`, `Headers`) set many at once. ```go -res, err := httpx.Get[map[string]any](c, "https://httpbin.org/get") -if err != nil { - return err +type GetResponse struct { + URL string `json:"url"` +} +type CreateUser struct { + Name string `json:"name"` +} +type CreateUserResponse struct { + JSON CreateUser `json:"json"` } -_ = res + +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" ``` ## Browser Profiles @@ -138,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) + // handle error } -_ = res +httpx.Dump(res) // dumps map[string]any ``` ## Debugging and Tracing @@ -162,6 +174,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 @@ -722,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 { @@ -890,6 +906,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") @@ -921,10 +938,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 @@ -934,11 +948,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 79c1e72..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" // // } @@ -447,6 +441,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") 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" // } 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.