Go (net/http)
Idiomatic net/http with request contexts, struct decoding, and back-off on 429.
Go shipsnet/http with everything you need to call a JSON API in production: configurable transports, request contexts for cancellation and deadlines, and a streaming body reader that plays nicely withencoding/json. You almost never need a third-party client. This guide walks through the Go REST endpoints with patterns you can copy into a real service.
Setup
Define a small client struct that holds the base URL, the access token, and a long-lived*http.Client. Reusing the client is important; every freshhttp.Client opens new TCP+TLS connections, which adds 100+ ms per request to a public API.
package gorest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
type Client struct {
BaseURL string
Token string
HTTP *http.Client
}
func New(token string) *Client {
return &Client{
BaseURL: "https://gorest.co.in/public/v2",
Token: token,
HTTP: &http.Client{Timeout: 15 * time.Second},
}
}
Send the bearer token
Wrap the request creation so you cannot forget theAuthorization header. Always pass a context; even an immediate timeout from the caller is useful when the upstream is slow.
func (c *Client) newRequest(ctx context.Context, method, path string, body any) (*http.Request, error) {
var rdr io.Reader
if body != nil {
buf, err := json.Marshal(body)
if err != nil { return nil, err }
rdr = bytes.NewReader(buf)
}
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, rdr)
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return req, nil
}
List users
Define a struct that matches the shape the API returns, then decode straight into it. Build the query string withurl.Values rather than concatenating strings.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Gender string `json:"gender"`
Status string `json:"status"`
}
func (c *Client) ListUsers(ctx context.Context, status string, page int) ([]User, int, error) {
q := url.Values{}
q.Set("status", status)
q.Set("page", strconv.Itoa(page))
req, err := c.newRequest(ctx, "GET", "/users?"+q.Encode(), nil)
if err != nil { return nil, 0, err }
res, err := c.HTTP.Do(req)
if err != nil { return nil, 0, err }
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, 0, fmt.Errorf("list users: %s", res.Status)
}
var users []User
if err := json.NewDecoder(res.Body).Decode(&users); err != nil {
return nil, 0, err
}
totalPages, _ := strconv.Atoi(res.Header.Get("X-Pagination-Pages"))
return users, totalPages, nil
}
Fetch a single user
var ErrNotFound = fmt.Errorf("not found")
func (c *Client) GetUser(ctx context.Context, id int) (*User, error) {
req, err := c.newRequest(ctx, "GET", fmt.Sprintf("/users/%d", id), nil)
if err != nil { return nil, err }
res, err := c.HTTP.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound { return nil, ErrNotFound }
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("get user %d: %s", id, res.Status)
}
var u User
return &u, json.NewDecoder(res.Body).Decode(&u)
}
Create, update, delete
The three writes use POST, PATCH and DELETE. Use a separate struct for the request body so you do not accidentally send fields the API does not expect (id in particular).
type UserInput struct {
Name string `json:"name"`
Email string `json:"email"`
Gender string `json:"gender"`
Status string `json:"status"`
}
func (c *Client) CreateUser(ctx context.Context, in UserInput) (*User, error) {
req, err := c.newRequest(ctx, "POST", "/users", in)
if err != nil { return nil, err }
res, err := c.HTTP.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("create user: %s", res.Status)
}
var u User
return &u, json.NewDecoder(res.Body).Decode(&u)
}
A 422 carries an array of validation errors. Decode it into a slice and bubble it up as a typed error so your callers can branch on it:
type FieldError struct {
Field string `json:"field"`
Message string `json:"message"`
}
type ValidationError struct{ Errors []FieldError }
func (v ValidationError) Error() string {
return fmt.Sprintf("validation: %d field(s) failed", len(v.Errors))
}
// ... inside CreateUser, before the 201 check:
if res.StatusCode == http.StatusUnprocessableEntity {
var errs []FieldError
if err := json.NewDecoder(res.Body).Decode(&errs); err != nil {
return nil, err
}
return nil, ValidationError{Errors: errs}
}
Retry on rate limits
Implement a small back-off in your transport rather than every caller. Use a customhttp.RoundTripper so retries are transparent. TheX-RateLimit-Reset header is in seconds:
type retryTransport struct{ base http.RoundTripper }
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for attempt := 0; attempt < 3; attempt++ {
res, err := t.base.RoundTrip(req)
if err != nil { return nil, err }
if res.StatusCode != http.StatusTooManyRequests { return res, nil }
wait, _ := strconv.Atoi(res.Header.Get("X-RateLimit-Reset"))
if wait < 1 { wait = 1 }
res.Body.Close()
select {
case <-time.After(time.Duration(wait) * time.Second):
case <-req.Context().Done():
return nil, req.Context().Err()
}
}
return nil, fmt.Errorf("rate limited; gave up")
}
// wire it on the client:
c.HTTP.Transport = &retryTransport{base: http.DefaultTransport}
Tips
- Always code= 'defer res.Body.Close()' | . Skipping it leaks file descriptors and prevents connection reuse.
json.NewDecoderis more efficient thanjson.Unmarshalfor HTTP responses, with no extra buffer.- The default code= 'http.Client' | has no timeout. Always set one (15 seconds is reasonable).
- Define request structs separately from response structs even when the fields overlap. Makes it harder to accidentally re-send id/created_at when updating.
Keep going
JavaScript (Fetch API)
Browser-native fetch with async/await, bearer-token auth, error handling, and pagination.
Node.js
Server-side requests with global fetch and axios: retries, env-loaded tokens, streaming JSON.
Python (requests)
Calls with the requests library, JSON bodies, query filtering, and dataclass parsing.