Skip to content

Resty

Web & HTTPNetworking/HTTPGo

What it is

Resty is a simple, chainable HTTP and REST client for Go, adding retries, middleware, automatic marshalling and response parsing on top of net/http.

Resty exposes a fluent builder for requests: set headers, query parameters, body and expected result type, then execute. It supports automatic retry with backoff, request and response middleware, and unmarshalling straight into a struct.

Installation

go get github.com/go-resty/resty/v2

Getting started

The smallest useful thing you can do with it, and what each part means.

A typed GET request
type Book struct {
    ID    string `json:"id"`
    Title string `json:"title"`
}

client := resty.New().
    SetBaseURL("https://api.example.com").
    SetTimeout(10 * time.Second)

var book Book
resp, err := client.R().
    SetContext(ctx).
    SetResult(&book).            // decoded automatically on 2xx
    Get("/books/42")

if err != nil {
    return fmt.Errorf("request failed: %w", err)
}
if resp.IsError() {
    return fmt.Errorf("api returned %d", resp.StatusCode())
}
SetResult decodes the body into your struct when the status is 2xx. Note that err is only non-nil for transport failures — a 500 response is a successful request, so IsError() must be checked separately.
POST with automatic retry
client := resty.New().
    SetRetryCount(3).
    SetRetryWaitTime(200 * time.Millisecond).
    SetRetryMaxWaitTime(2 * time.Second).
    AddRetryCondition(func(r *resty.Response, err error) bool {
        return err != nil || r.StatusCode() >= 500
    })

resp, err := client.R().
    SetBody(Book{Title: "Dune"}).
    SetHeader("Idempotency-Key", key).
    Post("/books")
Retries use exponential backoff with jitter. Only retry idempotent operations, or send an idempotency key so the server can deduplicate — otherwise a retried POST can create duplicate records.

Advanced usage

Where the library earns its place over a simpler alternative.

Middleware for auth and tracing
client.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error {
    token, err := tokens.Current(r.Context())
    if err != nil {
        return err
    }
    r.SetHeader("Authorization", "Bearer "+token)
    return nil
})

client.OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
    slog.Info("upstream call",
        "method", r.Request.Method,
        "url", r.Request.URL,
        "status", r.StatusCode(),
        "duration", r.Time())
    return nil
})
Centralising token refresh and logging in middleware means individual call sites stay clean, and you cannot forget to attach credentials on a new endpoint.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

SetResult struct is empty despite a 200 response
Resty decodes based on the Content-Type header. If the server returns text/plain, use SetHeader("Accept", "application/json") or decode resp.Body() manually.
context deadline exceeded
The client timeout or request context expired. Distinguish the two: SetTimeout covers the whole request, while the context may be cancelled by an upstream caller.

Best practices

  • Create one client and reuse it — a new client per request defeats connection pooling.
  • Always check resp.IsError(); err only reports transport-level failures, not HTTP error statuses.
  • Pass a context with SetContext so requests are cancelled when the caller goes away.
  • Restrict retries to idempotent requests, or attach an idempotency key.

Background

Why it exists, and what it was reacting to.

Resty was created by Jeevanandam M. to remove the repetitive boilerplate of building requests, checking status codes and decoding bodies with net/http. It keeps the standard client underneath, so timeouts and transports remain configurable.