- I was tired of writing
json:"fieldName,omitempty"
alongside each field. This package de-capitalizes the public fields in JSON parsing and omits empty ones unlessjson
tag is specified. - I always forget all the boilerplate code to make an HTTP request. This package provides a simple one-function call approach.
This is a zero-dependency package. It requires Go version 1.21 or above.
go get github.com/glossd/fetch
Functions of the fetch
package match HTTP methods. Each function is generic and its generic type is the response.
This is the Pet
object from https://petstore.swagger.io/
{
"id": 1,
"name": "Buster",
"tags": [
{
"name": "beagle"
}
]
}
str, err := fetch.Get[string]("https://petstore.swagger.io/v2/pet/1")
if err != nil {
//handle err
}
fmt.Println(str)
fetch.J
is an interface representing arbitrary JSON.
j, err := fetch.Get[fetch.J]("https://petstore.swagger.io/v2/pet/1")
if err != nil {
panic(err)
}
// access nested values using jq-like patterns
fmt.Println("Pet's name is ", j.Q(".name"))
fmt.Println("First tag's name is ", j.Q(".tags[0].name"))
type Tag struct {
Name string
}
type Pet struct {
Name string
Tags []Tag
}
pet, err := fetch.Get[Pet]("https://petstore.swagger.io/v2/pet/1")
if err != nil {
panic(err)
}
fmt.Println("Pet's name is ", pet.Name)
fmt.Println("First tag's name is ", pet.Tags[0].Name) // handle index
Post, Put and others have an additional argument for the request body of any
type.
type Pet struct {
Name string
}
type IdObj struct {
Id int
}
obj, err := fetch.Post[IdObj]("https://petstore.swagger.io/v2/pet", Pet{Name: "Lola"})
if err != nil {
panic(err)
}
fmt.Println("Posted pet's ID ", obj.Id)
*Passing string
or []byte
type variable as the second argument will directly add its value to the request body.
If you need to check the status or headers of the response, you can wrap your response type with fetch.Response
.
type Pet struct {
Name string
}
resp, err := fetch.Get[fetch.Response[Pet]]("https://petstore.swagger.io/v2/pet/1")
if err != nil {
panic(err)
}
if resp.Status == 200 {
fmt.Println("Found pet with id 1")
// Response.Body is the Pet object.
fmt.Println("Pet's name is ", resp.Body.Name)
fmt.Println("Response headers", resp.Headers)
}
If you don't need the HTTP body you can use fetch.Empty
or fetch.Response[fetch.Empty]
to access http attributes
res, err := fetch.Delete[fetch.Response[fetch.Empty]]("https://petstore.swagger.io/v2/pet/10")
if err != nil {
panic(err)
}
fmt.Println("Status:", res.Status)
fmt.Println("Headers:", res.Headers)
Any non-2xx response status is treated as an error!
If the error isn't nil
it can be safely cast to *fetch.Error
which will contain the status and other HTTP attributes.
_, err := fetch.Get[string]("https://petstore.swagger.io/v2/pet/-1")
if err != nil {
fmt.Printf("Get pet failed: %s\n", err)
ferr := err.(*fetch.Error)
fmt.Printf("HTTP status=%d, headers=%v, body=%s", ferr.Status, ferr.Headers, ferr.Body)
}
Request Context lives in fetch.Config
func myFuncWithContext(ctx context.Context) {
...
res, err := fetch.Get[string]("https://petstore.swagger.io/v2/pet/1", fetch.Config{Ctx: ctx})
...
}
Request with 5 seconds timeout:
fetch.Get[string]("https://petstore.swagger.io/v2/pet/1", fetch.Config{Timeout: 5*time.Second})
headers := map[string]string{"Content-type": "text/plain"}
_, err := fetch.Get[string]("https://petstore.swagger.io/v2/pet/1", fetch.Config{Headers: headers})
if err != nil {
panic(err)
}
If you want this package to parse the public fields as capitalized into JSON, you need to add the json
tag:
type Pet struct {
Name string `json:"Name"`
}
Simple, just pass an array as the response type.
type Pet struct {
Name string
}
pets, err := fetch.Get[[]Pet]("https://petstore.swagger.io/v2/pet/findByStatus?status=sold")
if err != nil {
panic(err)
}
fmt.Println("First sold pet ", pets[0]) // handle index out of range
Or you can use fetch.J
j, err := fetch.Get[fetch.J]("https://petstore.swagger.io/v2/pet/findByStatus?status=sold")
if err != nil {
panic(err)
}
fmt.Println("First sold pet ", j.Q(".[0]"))
fetch.J
is an interface with Q
method which provides easy access to any field.
Method fetch.J#String()
returns JSON formatted string of the value.
j := fetch.Parse(`{
"name": "Jason",
"category": {
"name":"dogs"
},
"tags": [
{"name":"briard"}
]
}`)
fmt.Println("Print the whole json:", j)
fmt.Println("Pet's name is", j.Q(".name"))
fmt.Println("Pet's category name is", j.Q(".category.name"))
fmt.Println("First tag's name is", j.Q(".tags[0].name"))
Method fetch.J#Q
returns fetch.J
. You can use the method Q
on the result as well.
category := j.Q(".category")
fmt.Println("Pet's category object", category)
fmt.Println("Pet's category name is", category.Q(".name"))
To convert fetch.J
to a basic value use one of As*
methods
J Method | Return type |
---|---|
AsObject | map[string]any |
AsArray | []any |
AsNumber | float64 |
AsString | string |
AsBoolean | bool |
E.g.
n, ok := fetch.Parse(`{"price": 14.99}`).Q(".price").AsNumber()
if !ok {
// not a number
}
fmt.Printf("Price: %.2f\n", n) // n is a float64
Use IsNil
to check the value on presence.
if fetch.Parse("{}").Q(".price").IsNil() {
fmt.Println("key 'price' doesn't exist")
}
// fields of unknown values are nil as well.
if fetch.Parse("{}").Q(".price.cents").IsNil() {
fmt.Println("'cents' of undefined is fine.")
}
I have patched encoding/json
package and attached to the internal
folder, but you can use these functions.
Use it to convert any object into a string. It's the same as json.Marshal
but it treats public struct fields as de-capitalized and omits empty fields by default unless json
tag is specified.
str, err := fetch.Marhsal(map[string]string{"key":"value"})
Unmarshal will parse the input into the generic type.
type Pet struct {
Name string
}
p, err := fetch.Unmarshal[Pet](`{"name":"Jason"}`)
if err != nil {
panic(err)
}
fmt.Println(p.Name)
fetch.Parse
unmarshalls JSON string into fetch.J
, returning fetch.Nil
instead of an error,
which allows you to write one-liners.
fmt.Println(fetch.Parse(`{"name":"Jason"}`).Q(".name"))
You can set base URL path for all requests.
fetch.SetBaseURL("https://petstore.swagger.io/v2")
pet := fetch.Get[string]("/pets/1")
// you can still call other URLs by passing URL with protocol.
fetch.Get[string]("https://www.google.com")
You can set the http.Client for all requests
fetch.SetHttpClient(&http.Client{Timeout: time.Minute})
Each HTTP method has the configuration option.
type Config struct {
// Defaults to context.Background()
Ctx context.Context
// Sets Ctx with the specified timeout. If Ctx is specified Timeout is ignored.
Timeout time.Duration
// Defaults to GET
Method string
Body string
Headers map[string]string
}
fetch.ToHandlerFunc
converts func(in) (out, error)
signature function into http.HandlerFunc
. It does all the json and http handling for you.
The HTTP request body unmarshalls into the function argument. The return value is marshaled into the HTTP response body.
type Pet struct {
Name string
}
http.HandleFunc("/pets", fetch.ToHandlerFunc(func(in Pet) (*Pet, error) {
if in.Name == "" {
return nil, fmt.Errorf("name can't be empty")
}
return &Pet{Name: in.Name}, nil
}))
http.ListenAndServe(":8080", nil)
If you have empty request or response body or you want to ignore them, use fetch.Empty
:
http.HandleFunc("/default-pet", fetch.ToHandlerFunc(func(_ fetch.Empty) (Pet, error) {
return Pet{Name: "Teddy"}, nil
}))
If you need to access http request attributes wrap the input with fetch.Request
:
type Pet struct {
Name string
}
http.HandleFunc("/pets", fetch.ToHandlerFunc(func(req fetch.Request[Pet]) (*fetch.Empty, error) {
fmt.Println("Request context:", req.Context)
fmt.Println("Authorization header:", req.Headers["Authorization"])
fmt.Println("Pet:", req.Body)
fmt.Println("Pet's name:", req.Body.Name)
return nil, nil
}))
If you have go1.23 and above you can access the wildcards as well.
http.HandleFunc("GET /pets/{id}", fetch.ToHandlerFunc(func(in fetch.Request[fetch.Empty]) (*fetch.Empty, error) {
fmt.Println("id from url:", in.PathValues["id"])
return nil, nil
}))
To customize http attributes of the response, wrap the output with fetch.Response
http.HandleFunc("/pets", fetch.ToHandlerFunc(func(_ fetch.Empty) (fetch.Response[*Pet], error) {
return Response[*Pet]{Status: 201, Body: &Pet{Name: "Lola"}}, nil
}))
The error format can be customized with the fetch.SetRespondErrorFormat
global setter.
To log http errors with your logger call SetDefaultHandlerConfig
fetch.SetDefaultHandlerConfig(fetch.HandlerConfig{ErrorHook: func(err error) {
mylogger.Errorf("fetch http error: %s", err)
}})
To add middleware before handling request in fetch.ToHandlerFunc
fetch.SetDefaultHandlerConfig(fetch.HandlerConfig{Middleware: func(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Authorization") == "" {
w.WriteHeader(401)
return true
}
return false
}})