Standardized, fluent, zero-dependency API response wrapping for Go REST services, with built-in pagination, tracing metadata, error stacks, a JSON path engine (
fj), and replify-native concurrent execution.
- Overview
- Installation
- Quick Start
- Usage Examples
- API / Configuration Reference
- Best Practices
- Architecture Notes
- Testing & Benchmarks
- Contributing
- License
Overview
replify removes the boilerplate of standardizing REST responses. It applies the Decorator Pattern through a fluent, chainable API so that error handling, metadata, pagination, debugging data, and signatures are layered onto a single response envelope with a consistent wire format across every endpoint.
Problems addressed and design rationale
| Problem | replify's answer |
|---|---|
| Inconsistent response formats across endpoints | One standardized envelope for all endpoints |
| Repetitive error-handling boilerplate in every handler | Fluent builders: WithError, WithErrorAck, AppendError, OK(), Accepted(), ... |
| Manual metadata management (request IDs, timestamps, versions) | First-class meta object: request ID, API version, locale, timestamps, custom fields |
| Pagination logic scattered across the codebase | Built-in pagination object (page, per_page, total_items, total_pages, is_last) |
| Debugging differences between production and development | Conditional debug object, opt-in per environment |
| Type safety | Full type safety with Go generics |
| Dependency footprint | Zero dependencies: Go standard library only |
Key features
- Standardized JSON format: a consistent structure across all API responses.
- Fluent builder pattern: chain methods to construct complex responses.
- Pagination support:
page,per_page,total_items,total_pages,is_last, plus a cursor object. - Request tracing: unique request IDs across microservices.
- Internationalization: locale support for multi-language APIs.
- Debug mode: conditional debugging information for development.
- Error handling: rich error information with stack traces and error wrapping.
- Metadata: API version, custom fields, timestamps.
- Status helpers:
IsSuccess(),IsClientError(),IsServerError(), and more. - JSON parsing: parse JSON strings back into wrapper objects (
UnwrapJSON). - Framework-independent writers:
Write,WriteJSON,WriteFile,WriteBinaryagainsthttp.ResponseWriter(net/http, Gin, Echo, Fiber, ...). fjJSON engine: path-based extraction, search, aggregation, and transformers without full unmarshalling.- Concurrent execution:
WorkerGroup/Poolfan-out and persistent worker pools with replify-native aggregated results.
When to use vs. when not to use
Use replify for:
- RESTful API development: standardizing API responses.
- Microservices: consistent responses across services.
- API versioning: including version metadata.
- Error standardization: consistent error formats.
- Pagination: APIs returning paginated results.
- Multi-tenant APIs: including tenant/locale information.
- Request tracing: tracking requests across services.
- Development debugging: conditional debug information.
Do not use replify for:
- GraphQL APIs: GraphQL has its own response format.
- gRPC services: Protocol Buffers define the structure.
- WebSocket APIs: real-time bidirectional communication.
- Simple CLIs: overkill for command-line tools.
- Internal services where custom formats are required.
- High-performance paths: direct JSON encoding may be faster.
Related packages in the replify ecosystem
| Package | Purpose |
|---|---|
| replify | API response wrapping library (this package) |
| conv | Type conversion utilities |
| coll | Type-safe collection utilities |
| common | Reflection-based utilities |
| encoding | JSON encoding utilities |
| hashy | Deterministic hashing |
| match | Wildcard pattern matching |
| msort | Map sorting utilities |
| randn | Random data generation |
| ref | Pointer utilities |
| strutil | String utilities |
| truncate | String truncation utilities |
| workergroup | OOP-style, live-scalable concurrency (error groups and worker pools) |
Installation
Prerequisites: Go 1.24 or higher (go.mod declares go 1.24.0). With Go modules, go build|run|test fetches the dependency automatically once the import is added.
Install the latest version
go get github.com/polarixa/replify@latest
# go: added github.com/polarixa/replify vX.Y.ZInstall a specific version
go get github.com/polarixa/replify@v0.1.0
# go: added github.com/polarixa/replify v0.1.0Import in code
package main
import (
"fmt"
"github.com/polarixa/replify"
)
func main() {
fmt.Println(replify.New().OK().StatusCode()) // 200
}Quick Start
Minimal runnable snippet
package main
import (
"fmt"
"github.com/polarixa/replify"
)
func main() {
w := replify.New().OK().
WithMessage("User retrieved successfully").
WithBody(map[string]string{"id": "123", "name": "John Doe"})
fmt.Println(w.JSON()) // {"data":{"id":"123","name":"John Doe"},"header":{"code":200,"text":"OK","type":"Successful"},"message":"User retrieved successfully","meta":{"api_version":"v0.0.1","locale":"en_US","request_id":"db848ff9djinl03ik1d0","requested_time":"2026-10-15 10:10:53.721643"},"status_code":200,"total":0}
}Output
{
"data": {
"id": "123",
"name": "John Doe"
},
"header": {
"code": 200,
"text": "OK",
"type": "Successful"
},
"message": "User retrieved successfully",
"meta": {
"api_version": "v0.0.1",
"locale": "en_US",
"request_id": "db848ff9djinl03ik1d0",
"requested_time": "2026-10-15 10:10:53.721643"
},
"status_code": 200,
"total": 0
}Usage Examples
Example: Writing responses to http.ResponseWriter (JSON, file, binary, Gin)
All writers are framework-independent and accept the standard http.ResponseWriter, so they work with net/http, Gin, Echo, Fiber, and any framework whose writer implements that interface. Implementation uses only net/http, mime, path/filepath, os, and io.
Native net/http handler (Write dispatch)
Write dispatches automatically: filepath set → WriteFile; data is []byte → WriteBinary; otherwise → WriteJSON. Content-Type and status code are set for you.
package main
import (
"log"
"net/http"
"github.com/polarixa/replify"
)
func GetUser(response http.ResponseWriter, request *http.Request) {
user := map[string]string{"id": "123", "name": "John Doe"}
w := replify.New().
OK().
WithBody(user).
WithMessage("User retrieved successfully").
Write(response)
w.Slogging()
if w.IsErrorPresent() {
w.S().Errorf("Error writing response: %v", w.Error())
}
}
func main() {
http.HandleFunc("/user", GetUser)
log.Fatal(http.ListenAndServe(":8080", nil)) // GET /user -> 200 application/json {"data":{"id":"123",...}}
}JSON
package main
import (
"net/http"
"net/http/httptest"
"github.com/polarixa/replify"
)
func main() {
user := map[string]string{"id": "123"}
rec := httptest.NewRecorder()
w := replify.New().
WithStatusCode(http.StatusOK).
WithBody(user).
WithMessage("User retrieved successfully").
Write(rec)
if w.IsErrorPresent() {
panic(w.Error())
}
println(rec.Code, rec.Header().Get("Content-Type")) // 200 application/json
}File
package main
import (
"net/http"
"github.com/polarixa/replify"
)
func Report(w http.ResponseWriter, r *http.Request) {
// 200, Content-Type detected from the file extension (application/pdf)
replify.New().
File("/tmp/report.pdf").
Write(w)
}
func main() { http.HandleFunc("/report", Report) }File attachment
Serves the file with Content-Disposition: attachment so the browser offers a download dialog.
package main
import (
"net/http"
"github.com/polarixa/replify"
)
func Download(w http.ResponseWriter, r *http.Request) {
// Content-Disposition: attachment; filename=report.pdf
replify.New().
FileAttachment("/tmp/report.pdf", "report.pdf").
Write(w)
}
func main() { http.HandleFunc("/download", Download) }Binary, binary with filename, and custom status code
The filename drives MIME-type detection and the Content-Disposition header.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/polarixa/replify"
)
func main() {
data := []byte(`{"ok":true}`)
rec := httptest.NewRecorder()
replify.New().Binary(data).Write(rec)
fmt.Println(rec.Code) // 200 (application/octet-stream)
rec = httptest.NewRecorder()
replify.New().Binary(data).Filename("report.pdf").Write(rec)
fmt.Println(rec.Header().Get("Content-Type")) // application/pdf
rec = httptest.NewRecorder()
replify.New().WithStatusCode(http.StatusCreated).Binary(data).Filename("result.json").Write(rec)
fmt.Println(rec.Code) // 201
}Gin
replify does not import Gin; it depends only on http.ResponseWriter. Gin's c.Writer implements it, so pass it directly (the consuming module must depend on github.com/gin-gonic/gin).
package main
import (
"github.com/gin-gonic/gin"
"github.com/polarixa/replify"
)
func Download(c *gin.Context) {
replify.New().
FileAttachment("/tmp/report.pdf", "report.pdf").
Write(c.Writer) // 200 with Content-Disposition: attachment
}
func main() {
r := gin.Default()
r.GET("/download", Download)
}Dispatch rules and safe filename handling
| Condition | Writer called |
|---|---|
filepath is set |
WriteFile |
data is []byte |
WriteBinary |
| otherwise | WriteJSON |
No response-type field is introduced; the decision is made purely from existing wrapper state.
Filenames used in Content-Disposition are validated and encoded by the standard mime package (RFC 5987). Filenames containing CR, LF, or null bytes are rejected with an error to prevent HTTP response-header injection.
Example: Building responses, metadata, pagination, and debugging
Success, error, and metadata responses
package main
import (
"fmt"
"github.com/polarixa/replify"
)
func main() {
data := map[string]string{"id": "1"}
ok := replify.New().
WithStatusCode(200).
WithMessage("Operation successful").
WithBody(data)
bad := replify.New().
WithStatusCode(400).
WithError("Invalid input: email is required").
WithMessage("Validation failed")
meta := replify.New().
WithStatusCode(200).
WithBody([]string{"alice"}).
WithRequestID("req-123-456").
WithApiVersion("v1.0.0").
WithLocale("en_US").
WithPath("/api/v1/users")
fmt.Println(ok.IsSuccess(), bad.IsClientError(), meta.Meta().IsRequestIDPresent()) // true true true
}Pagination
package main
import (
"fmt"
"github.com/polarixa/replify"
)
func main() {
pagination := replify.Pages().
WithPage(1).
WithPerPage(20).
WithTotalItems(150).
WithTotalPages(8).
WithIsLast(false)
w := replify.New().
WithStatusCode(200).
WithBody([]string{"alice", "bob"}).
WithPagination(pagination).
WithTotal(20)
fmt.Println(w.IsPagingPresent(), w.IsLastPage()) // true false
}Debugging information
package main
import (
"fmt"
"github.com/polarixa/replify"
)
func main() {
w := replify.New().
WithStatusCode(500).
WithError("Database connection failed").
WithDebuggingKV("query", "SELECT * FROM users").
WithDebuggingKV("error_code", "CONN_TIMEOUT").
WithDebuggingKV("retry_count", 3)
fmt.Println(w.OnDebugging("error_code")) // CONN_TIMEOUT
}Complete example (pagination, debugging, custom fields, accessors)
package main
import (
"fmt"
"github.com/polarixa/replify"
"github.com/polarixa/replify/pkg/randn"
)
func main() {
p := replify.Pages().
WithIsLast(true).
WithPage(1000).
WithTotalItems(120).
WithTotalPages(34).
WithPerPage(2)
w := replify.New().
WithStatusCode(200).
WithTotal(1).
WithMessagef("How are you? %v", "I'm good").
WithDebuggingKV("refer", 1234).
WithDebuggingKVf("___abc", "trace sessions_id: %v", randn.CryptoID()).
WithBody("response body here").
WithPath("/api/v1/users").
WithCustomFieldKVf("fields", "userID: %v", 103).
WithPagination(p)
if !w.Available() {
return
}
fmt.Println(w.JSON())
fmt.Println(w.StatusCode()) // 200
fmt.Println(w.StatusText()) // OK
fmt.Println(w.Message()) // How are you? I'm good
fmt.Println(w.Body()) // response body here
fmt.Println(w.IsSuccess()) // true
fmt.Println(w.Respond()) // map[...]
fmt.Println(w.Meta().IsCustomPresent()) // true
fmt.Println(w.Meta().IsApiVersionPresent()) // true
fmt.Println(w.Meta().IsRequestIDPresent()) // true
fmt.Println(w.Meta().IsRequestedTimePresent()) // true
}Paginated list endpoint
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"github.com/polarixa/replify"
)
func queryInt(r *http.Request, key string, def int) int {
if v, err := strconv.Atoi(r.URL.Query().Get(key)); err == nil && v > 0 {
return v
}
return def
}
func findUsers(search string, page, perPage int) ([]string, int, error) {
return []string{"alice", "bob"}, 120, nil
}
func ListUsers(w http.ResponseWriter, r *http.Request) {
page := queryInt(r, "page", 1)
perPage := queryInt(r, "per_page", 10)
search := r.URL.Query().Get("search")
users, total, err := findUsers(search, page, perPage)
if err != nil {
replify.New().
WithStatusCode(500).
WithErrorAck(err).
WithMessage("Failed to fetch users").
WithDebuggingKV("search", search).
WithDebuggingKV("page", page).
Write(w)
return
}
totalPages := (total + perPage - 1) / perPage
pagination := replify.Pages().
WithPage(page).
WithPerPage(perPage).
WithTotalItems(total).
WithTotalPages(totalPages).
WithIsLast(page >= totalPages)
replify.New().
WithStatusCode(200).
WithBody(users).
WithPagination(pagination).
WithTotal(len(users)).
WithMessage("Users retrieved successfully").
WithPath(r.URL.Path).
WithRequestID(r.Header.Get("X-Request-ID")).
Write(w)
}
func main() {
rec := httptest.NewRecorder()
ListUsers(rec, httptest.NewRequest(http.MethodGet, "/users?page=1&per_page=2", nil))
fmt.Println(rec.Code) // 200
}RESTful CRUD (GET and POST)
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/polarixa/replify"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func findUserByID(id string) (*User, error) {
if id == "1" {
return &User{ID: 1, Name: "Alice", Email: "alice@example.com"}, nil
}
return nil, errors.New("user not found")
}
// GET /users/{id}
func GetUser(w http.ResponseWriter, r *http.Request) {
user, err := findUserByID(r.PathValue("id"))
if err != nil {
replify.New().
WithStatusCode(404).
WithError(err.Error()).
WithMessage("User not found").
WithRequestID(r.Header.Get("X-Request-ID")).
Write(w)
return
}
replify.New().
WithStatusCode(200).
WithBody(user).
WithMessage("User retrieved successfully").
WithRequestID(r.Header.Get("X-Request-ID")).
Write(w)
}
// POST /users
func CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
replify.New().WithStatusCode(400).WithError(err.Error()).WithMessage("Invalid request body").Write(w)
return
}
if user.Email == "" {
replify.New().WithStatusCode(422).WithError("email is required").WithMessage("Validation failed").Write(w)
return
}
replify.New().WithStatusCode(201).WithBody(user).WithMessage("User created successfully").Write(w)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", GetUser)
mux.HandleFunc("POST /users", CreateUser)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/users/999", nil))
fmt.Println(rec.Code) // 404
rec = httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(`{"name":"Bob"}`)))
fmt.Println(rec.Code) // 422
}Example: Error handling with stack traces and environment-gated debugging
package main
import (
"errors"
"fmt"
"os"
"time"
"github.com/polarixa/replify"
)
type Order struct{ ID string }
func processOrderLogic() (*Order, error) {
return nil, errors.New("payment gateway unreachable")
}
func main() {
order, err := processOrderLogic()
response := replify.New()
if err != nil {
response.
WithStatusCode(500).
WithErrorAck(err). // attaches a stack trace; errors.Is/As keep working via Cause()
WithMessage("Order processing failed")
if os.Getenv("ENV") == "development" {
response.
WithDebuggingKV("timestamp", time.Now()).
WithDebuggingKV("stack_trace", err.Error()).
WithDebuggingKV("order_data", order)
}
} else {
response.WithStatusCode(200).WithBody(order).WithMessage("Order processed successfully")
}
fmt.Println(response.StatusCode(), response.IsServerError()) // 500 true
}Example: Parsing JSON into a wrapper
package main
import (
"fmt"
"log"
"time"
"github.com/polarixa/replify"
)
func main() {
jsonStr := `{
"data": "response body here",
"debug": {"___abc": "trace sessions_id: 4919e84fc26881e9fe790f5d07465db4", "refer": 1234},
"message": "How do you do? I'm good",
"meta": {
"api_version": "v0.0.1",
"custom_fields": {"fields": "userID: 103"},
"locale": "en_US",
"request_id": "80eafc6a1655ec5a06595d155f1e6951",
"requested_time": "2024-12-14T20:24:23.983839+07:00"
},
"pagination": {"is_last": true, "page": 1000, "per_page": 2, "total_items": 120, "total_pages": 34},
"path": "/api/v1/users",
"status_code": 200,
"total": 1
}`
t := time.Now()
w, err := replify.UnwrapJSON(jsonStr)
diff := time.Since(t)
if err != nil {
log.Fatalf("Error parsing JSON: %v", err)
}
fmt.Printf("Exe time: %+v\n", diff.String()) // Exe time: <duration>
fmt.Printf("%+v\n", w.OnDebugging("___abc")) // trace sessions_id: 4919e84fc26881e9fe790f5d07465db4
fmt.Printf("%+v\n", w.JSONPretty()) // indented JSON of the wrapper
}Example: fj JSON path extraction, wrapper integration, and transformers
fj (Fast JSON), in pkg/fj, reads, queries, and transforms values from a JSON document without unmarshalling the whole structure. It walks the raw string only far enough to locate the requested path, keeping allocations low on hot request paths.
When to use fj instead of encoding/json
| Scenario | Recommended approach |
|---|---|
| Extract one or a few fields from a large response body | fj / QueryJSONBody |
| Validate that the body is well-formed JSON | fj.IsValidJSON / ValidJSONBody |
| Search leaf values or keys across an unknown schema | fj.Search / SearchJSONBody* |
| Apply streaming transforms (pretty-print, minify, etc.) | fj transformers |
| Bind the full payload into a typed struct | encoding/json or json-iterator |
| Write or modify JSON | encoding/json |
| JSON schema validation | a dedicated schema library |
Path syntax quick reference
user.name field access
roles.0 array index
roles.# array length
roles.#.name collect field from every element
roles.#(role=="admin") first element where role == "admin"
roles.#(role=="admin")# all elements where role == "admin"
{id,name} multi-selector -> new object
[id,name] multi-selector -> new array
name.@uppercase built-in transformer
name.@word:upper transformer with argument
..title recursive descent (JSON Lines / deep scan)
Dots and wildcards in key names can be escaped with a backslash (\).
Direct fj usage
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
json := `{
"user": {"name": "Alice", "age": 30, "active": true},
"roles": ["admin", "editor"],
"scores": [95, 87, 92]
}`
fmt.Println(fj.Get(json, "user.name").String()) // Alice
fmt.Println(fj.Get(json, "user.age").Int64()) // 30
fmt.Println(fj.Get(json, "user.active").Bool()) // true
fmt.Println(fj.Get(json, "roles.#").Int()) // 2
results := fj.GetMulti(json, "user.name", "user.age", "roles.#")
fmt.Println(results[0].String(), results[1].Int64(), results[2].Int()) // Alice 30 2
if ctx := fj.Get(json, "user.email"); ctx.Exists() {
fmt.Println(ctx.String())
} else {
fmt.Println("user.email absent") // user.email absent
}
// Parse once, query many times.
doc := fj.Parse(json)
fmt.Println(doc.Get("user.name").String(), doc.Get("roles.0").String()) // Alice admin
}Zero-copy byte-slice access
GetBytes is preferred when you already hold a []byte; it uses unsafe pointer operations internally to avoid an extra string allocation.
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
rawBytes := []byte(`{"id":42,"status":"active"}`)
fmt.Println(fj.GetBytes(rawBytes, "id").Int()) // 42
fmt.Println(fj.GetBytes(rawBytes, "status").String()) // active
res := fj.GetBytesMulti(rawBytes, "id", "status")
fmt.Println(len(res)) // 2
}Memory note:
fj.Context.Raw()returns a substring view of the original string without copying. Do not hold aContextafter the source string has been released.
Wrapper integration (QueryJSONBody, aggregates)
The wrapper exposes all fj operations (see parser.go) so most code need not import pkg/fj.
package main
import (
"fmt"
"log"
"github.com/polarixa/replify"
)
func main() {
response := replify.New().
WithStatusCode(200).
WithBody(map[string]any{
"user": map[string]any{"name": "Alice", "role": "admin"},
"items": []map[string]any{
{"id": 1, "price": 9.99},
{"id": 2, "price": 4.50},
},
})
name := response.QueryJSONBody("user.name").String()
fields := response.QueryJSONBodyMulti("user.name", "user.role") // one serialization
ctx := response.JSONBodyParser() // parse once, reuse
if !response.ValidJSONBody() {
log.Println("body is not valid JSON")
return
}
total := response.SumJSONBody("items.#.price")
min, minOK := response.MinJSONBody("items.#.price")
max, maxOK := response.MaxJSONBody("items.#.price")
avg, avgOK := response.AvgJSONBody("items.#.price")
fmt.Println(name) // Alice
fmt.Println(fields[0].String(), fields[1].String()) // Alice admin
fmt.Println(ctx.Get("user.name").String()) // Alice
fmt.Println(ctx.Get("items.#").Int()) // 2
fmt.Println(total) // 14.49
fmt.Println(min, minOK) // 4.5 true
fmt.Println(max, maxOK) // 9.99 true
fmt.Println(avg, avgOK) // 7.245 true
}Performance tip:
QueryJSONBodyserializes the body on every call. For repeated queries, callJSONBodyParser()once and reuse thefj.Context.
Context value extraction
Every query returns a fj.Context. Call .Exists() before using a value whose path may be absent.
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
json := `{"list":["a","b"],"n":1.5}`
ctx := fj.Get(json, "optional.field")
fmt.Println(ctx.Exists()) // false
fmt.Println(ctx.IsError(), ctx.Cause() == "") // false true
list := fj.Get(json, "list")
fmt.Println(list.IsArray(), list.IsObject()) // true false
fmt.Println(list.Kind() == fj.JSON) // true
fmt.Println(fj.Get(json, "n").Float64()) // 1.5
fmt.Println(list.Raw()) // ["a","b"]
list.Foreach(func(key, val fj.Context) bool {
fmt.Println(val.String()) // a, then b
return true // return false to stop
})
}Other accessors: Kind() (fj.Null | fj.String | fj.Number | fj.True | fj.False | fj.JSON), String(), Bool(), Int(), Int64(), Float64(), Raw().
Transformers: reference
Transformers are applied with the @ prefix inside a path and receive the current JSON value. An optional argument follows a :.
path.@transformerName
path.@transformerName:argument
path.@transformerName:{"key":"value"}
Core transformers
| Transformer | Alias(es) | Input | Description |
|---|---|---|---|
@pretty |
— | any | Pretty-print JSON. Accepts optional {"sort_keys":true,"indent":"\t","prefix":"","width":80}. |
@minify |
@ugly |
any | Compact single-line JSON (all whitespace removed). |
@valid |
— | any | Returns "true" / "false": whether the input is valid JSON. |
@this |
— | any | Identity; returns the input unchanged. |
@reverse |
— | array | object | Reverses element order (array) or key order (object). |
@flatten |
— | array | Shallow-flatten nested arrays. Pass {"deep":true} to recurse. |
@join |
— | array of objects | Merge an array of objects into one object. Pass {"preserve":true} to keep duplicate keys. |
@keys |
— | object | JSON array of the object's keys. |
@values |
— | object | JSON array of the object's values. |
@group |
— | object of arrays | Zip object-of-arrays into an array-of-objects. |
@search |
— | any | @search:path collects all values reachable at path anywhere in the tree. |
@json |
— | string | Parse the string as JSON and return the value. |
@string |
— | any | Encode the value as a JSON string literal. |
String transformers
| Transformer | Alias(es) | Description |
|---|---|---|
@uppercase |
@upper |
Convert all characters to upper-case. |
@lowercase |
@lower |
Convert all characters to lower-case. |
@flip |
— | Reverse the characters of the string. |
@trim |
— | Strip leading/trailing whitespace. |
@snakecase |
@snake, @snakeCase |
Convert to snake_case. |
@camelcase |
@camel, @camelCase |
Convert to camelCase. |
@kebabcase |
@kebab, @kebabCase |
Convert to kebab-case. |
@replace |
— | @replace:{"target":"old","replacement":"new"} replaces the first occurrence. |
@replaceAll |
— | @replaceAll:{"target":"old","replacement":"new"} replaces all occurrences. |
@hex |
— | Hex-encode the value. |
@bin |
— | Binary-encode the value. |
@insertAt |
— | @insertAt:{"index":5,"insert":"XYZ"} inserts a substring at a position. |
@wc |
— | Word count of a string as an integer. |
@padLeft |
— | @padLeft:{"padding":"*","length":10} left-pads to a fixed width. |
@padRight |
— | @padRight:{"padding":"*","length":10} right-pads to a fixed width. |
Object transformers
| Transformer | Description |
|---|---|
@project |
Pick and/or rename fields. Arg: {"pick":["f1","f2"],"rename":{"f1":"newName"}}. Omit pick to keep all fields; omit rename for no renaming. |
@default |
Inject fallback values for fields that are absent or null. Arg: {"field":"defaultValue",...}. Existing non-null fields are never overwritten. |
Array transformers
| Transformer | Description |
|---|---|
@filter |
Keep elements matching a condition. Arg: {"key":"field","op":"eq","value":val}. Operators: eq (default), ne, gt, gte, lt, lte, contains. |
@pluck |
Extract a named field (dot-notation supported) from every element, e.g. @pluck:name or @pluck:addr.city. |
@first |
First element, or null if empty. |
@last |
Last element, or null if empty. |
@count |
Number of elements (array) or key-value pairs (object). Scalars return 0. |
@sum |
Sum of numeric values; non-numeric elements are skipped. 0 for empty arrays. |
@min |
Minimum numeric value; null when no numbers are present. |
@max |
Maximum numeric value; null when no numbers are present. |
Value normalization transformers
| Transformer | Description |
|---|---|
@coerce |
Convert a scalar to a target type. Arg: {"to":"string"}, {"to":"number"}, or {"to":"bool"}. Objects and arrays are returned unchanged. |
Transformers: basic examples
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
json := `{
"user": {"name": "Alice", "role": null, "age": 30, "city": "NY"},
"scores": [95, 87, 92, 78],
"users": [
{"name": "Alice", "active": true, "addr": {"city": "NY"}},
{"name": "Bob", "active": false, "addr": {"city": "LA"}},
{"name": "Carol", "active": true, "addr": {"city": "NY"}}
]
}`
// Core
fmt.Println(fj.Get(json, "user.@keys").String()) // ["name","role","age","city"]
fmt.Println(fj.Get(json, "user.@values").String()) // ["Alice",null,30,"NY"]
fmt.Println(fj.Get(json, "user.@valid").String()) // true
// String
fmt.Println(fj.Get(json, "user.name.@uppercase").String()) // ALICE
fmt.Println(fj.Get(json, "user.name.@reverse").String()) // ecilA
// Object
fmt.Println(fj.Get(json, `user.@project:{"pick":["name","age"],"rename":{"age":"years"}}`).Raw()) // {"name":"Alice","years":30}
fmt.Println(fj.Get(json, `user.@default:{"role":"viewer","active":true}`).Raw()) // role filled with "viewer", active added
// Array
fmt.Println(fj.Get(json, `users.@filter:{"key":"active","value":true}`).Raw()) // Alice and Carol records
fmt.Println(fj.Get(json, `users.@pluck:addr.city`).Raw()) // ["NY","LA","NY"]
fmt.Println(fj.Get(json, "scores.@first").Raw()) // 95
fmt.Println(fj.Get(json, "scores.@last").Raw()) // 78
fmt.Println(fj.Get(json, "scores.@count").Raw()) // 4
fmt.Println(fj.Get(json, "scores.@sum").Raw()) // 352
fmt.Println(fj.Get(json, "scores.@min").Raw()) // 78
fmt.Println(fj.Get(json, "scores.@max").Raw()) // 95
// Coerce
fmt.Println(fj.Get(`42`, `@coerce:{"to":"string"}`).Raw()) // "42"
fmt.Println(fj.Get(`"99"`, `@coerce:{"to":"number"}`).Raw()) // 99
fmt.Println(fj.Get(`1`, `@coerce:{"to":"bool"}`).Raw()) // true
// Composition with the | pipe operator
fmt.Println(fj.Get(json, `users.@filter:{"key":"active","value":true}|@count`).Raw()) // 2
fmt.Println(fj.Get(json, `users.@pluck:name|@reverse`).Raw()) // ["Carol","Bob","Alice"]
}Transformers: complex real-world compositions
E-commerce catalog: filter, aggregate, shape
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
catalog := `{"products": [
{"id":"p1","name":"Laptop Pro", "category":"electronics","price":1299.99,"stock":5},
{"id":"p2","name":"USB-C Hub", "category":"electronics","price":49.99, "stock":120},
{"id":"p3","name":"Desk Chair", "category":"furniture", "price":349.00, "stock":0},
{"id":"p4","name":"Standing Desk", "category":"furniture", "price":699.00, "stock":3},
{"id":"p5","name":"Webcam HD", "category":"electronics","price":89.99, "stock":45}
]}`
fmt.Println(fj.Get(catalog, `products.@filter:{"key":"category","value":"electronics"}|@filter:{"key":"stock","op":"gt","value":0}|@pluck:name`).Raw()) // ["Laptop Pro","USB-C Hub","Webcam HD"]
fmt.Println(fj.Get(catalog, `products.@filter:{"key":"stock","op":"gt","value":0}|@count`).Raw()) // 4
fmt.Println(fj.Get(catalog, `products.@filter:{"key":"stock","op":"gt","value":0}|@pluck:price|@min`).Raw()) // 49.99
fmt.Println(fj.Get(catalog, `products.@filter:{"key":"stock","op":"gt","value":0}|@pluck:price|@max`).Raw()) // 1299.99
first := fj.Get(catalog, `products.@filter:{"key":"stock","op":"gt","value":0}|@first`).Raw()
fmt.Println(fj.Get(first, `@project:{"pick":["name","price"],"rename":{"name":"title","price":"cost"}}`).Raw()) // {"title":"Laptop Pro","cost":1299.99}
}API response normalization: defaults, project, rename
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
rawUser := `{"id":"u1","name":"Alice","role":null,"verified":null}`
fmt.Println(fj.Get(rawUser, `@default:{"role":"viewer","verified":false}|@project:{"pick":["id","name","role","verified"],"rename":{"id":"userId"}}`).Raw())
// {"userId":"u1","name":"Alice","role":"viewer","verified":false}
}Log processing: filter, count, latest entry
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
logs := `[
{"level":"error","msg":"Connection refused","ts":1700001},
{"level":"info", "msg":"Server started", "ts":1700002},
{"level":"error","msg":"Timeout exceeded", "ts":1700003},
{"level":"warn", "msg":"High memory", "ts":1700004}
]`
fmt.Println(fj.Get(logs, `@filter:{"key":"level","value":"error"}|@count`).Raw()) // 2
fmt.Println(fj.Get(logs, `@filter:{"key":"level","value":"error"}|@pluck:msg`).Raw()) // ["Connection refused","Timeout exceeded"]
fmt.Println(fj.Get(logs, `@filter:{"key":"level","value":"error"}|@last`).Raw()) // {"level":"error","msg":"Timeout exceeded","ts":1700003}
}Nested aggregation: filter, pluck, flatten, sum
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
teamData := `{"teams": [
{"name":"Alpha","active":true, "monthly_revenue":[10000,12000,11000]},
{"name":"Beta", "active":false,"monthly_revenue":[8000,9000,8500]},
{"name":"Gamma","active":true, "monthly_revenue":[15000,16000,14000]}
]}`
fmt.Println(fj.Get(teamData, `teams.@filter:{"key":"active","value":true}|@pluck:monthly_revenue|@flatten|@sum`).Raw())
// 78000 (Alpha: 33000 + Gamma: 45000)
}URL-slug generation
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
fmt.Println(fj.Get(`"My Blog Post Title"`, `@trim|@lowercase|@kebabcase`).Raw()) // "my-blog-post-title"
fmt.Println(fj.Get(`"John Doe"`, `@lowercase|@replace:{"target":" ","replacement":"-"}`).Raw()) // "john-doe"
}Config merging and introspection
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
overrides := `[{"host":"localhost","port":5432},{"port":5433,"ssl":true}]`
merged := fj.Get(overrides, `@join`).Raw()
fmt.Println(merged) // {"host":"localhost","port":5433,"ssl":true}
fmt.Println(fj.Get(merged, `@keys`).Raw()) // ["host","port","ssl"]
fmt.Println(fj.Get(merged, `@count`).Raw()) // 3
fmt.Println(fj.Get(merged, `@project:{"pick":["host","port"],"rename":{"port":"dbPort"}}`).Raw()) // {"host":"localhost","dbPort":5433}
}Leaderboard: zip parallel arrays, filter, pluck
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
leaderboard := `{"player":["Alice","Bob","Carol","Dave"],"score":[98,72,85,91]}`
grouped := fj.Get(leaderboard, `@group`).Raw()
fmt.Println(grouped) // [{"player":"Alice","score":98},{"player":"Bob","score":72},{"player":"Carol","score":85},{"player":"Dave","score":91}]
fmt.Println(fj.Get(grouped, `@filter:{"key":"score","op":"gte","value":85}|@pluck:player`).Raw()) // ["Alice","Carol","Dave"]
fmt.Println(fj.Get(grouped, `@filter:{"key":"score","op":"gte","value":95}|@first`).Raw()) // {"player":"Alice","score":98}
}Registering custom transformers
Transformers can be disabled globally with fj.DisableTransformers = true. The registry is package-level: register during initialization (init()), before concurrent access begins.
package main
import (
"fmt"
"github.com/polarixa/replify/pkg/fj"
)
func init() {
fj.AddTransformer("redact", fj.TransformerFunc(func(json, arg string) string {
return `"[REDACTED]"`
}))
}
func main() {
json := `{"user":{"password":"s3cret"}}`
fmt.Println(fj.Get(json, "user.password.@redact").String()) // [REDACTED]
}Search and scan helpers
package main
import (
"fmt"
"github.com/polarixa/replify"
)
func main() {
response := replify.New().WithStatusCode(200).WithBody(map[string]any{
"user": map[string]any{"name": "alice", "role": "admin", "email": "alice@example.com"},
"note": "error: disk full",
})
fmt.Println(response.SearchJSONBody("admin")) // leaf values containing "admin"
fmt.Println(response.SearchJSONBodyMatch("err*")) // leaf values matching wildcard
fmt.Println(response.SearchJSONBodyByKey("email")) // values under key "email"
fmt.Println(response.SearchJSONBodyByKeyPattern("user*")) // values under keys matching "user*"
fmt.Println(response.JSONBodyContains("user.role", "admin")) // true
fmt.Println(response.JSONBodyContainsMatch("user.email", "*@example.com")) // true
fmt.Println(response.FindJSONBodyPath("alice@example.com")) // user.email
fmt.Println(response.FindJSONBodyPathsMatch("err*")) // paths whose value matches, e.g. [note]
}Data manipulation helpers
package main
import (
"fmt"
"github.com/polarixa/replify"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
response := replify.New().WithStatusCode(200).WithBody(map[string]any{
"items": []int{1, 2, 3},
"tags": []string{"a", "b", "a"},
"users": []map[string]any{
{"id": 1, "email": "a@x.io", "role": "admin", "active": true},
{"id": 2, "email": "b@x.io", "role": "user", "active": false},
},
"products": []map[string]any{{"price": 9.5}, {"price": 3.0}},
})
fmt.Println(response.CountJSONBody("items")) // 3
fmt.Println(response.FilterJSONBody("users", func(ctx fj.Context) bool { return ctx.Get("active").Bool() }))
fmt.Println(response.FirstJSONBody("users", func(ctx fj.Context) bool { return ctx.Get("role").String() == "admin" }))
fmt.Println(response.DistinctJSONBody("tags")) // a, b (first-occurrence order)
fmt.Println(response.PluckJSONBody("users", "id", "email")) // projected rows
fmt.Println(response.GroupByJSONBody("users", "role")) // grouped by role
fmt.Println(response.SortJSONBody("products", "price", true)) // ordered by price
}fj limitations
- Read-only:
fjcannot write or modify JSON; useencoding/json. - No schema validation: use a dedicated library for strict schemas.
- No struct binding:
fjreturnsContextvalues, not typed structs. Raw()lifetime: the returned string is a zero-copy view into the source and must not outlive it.UnsafeBytes: the slice returned byfj.UnsafeBytesshares memory with the source string; never mutate it (violates Go string immutability, undefined behavior).- Malformed input:
fjdoes not validate JSON before parsing; pass untrusted input throughfj.IsValidJSONorValidJSONBody()first. - Transformers are global:
AddTransformerwrites to a package-level registry; register during init to avoid data races.
Example: Concurrent execution with WorkerGroup and Pool
pkg/workergroup (a dependency-free reimagining of golang.org/x/sync/errgroup with live-scalable concurrency and a persistent worker pool) is integrated as a first-class replify capability. WorkerGroup, Pool, RunWorkerGroup, and RunWorkerPool return a *wrapper that distinguishes full success, partial failure, group-level failure, and cancellation/timeout.
Core types
| Type / Function | Purpose |
|---|---|
WorkerFunc func(ctx context.Context) (any, error) |
A task's unit of work; the optional return value is preserved per-result |
WorkerTask / NewWorkerTask(name, fn) |
Pairs a name with a WorkerFunc for RunWorkerGroup |
PoolJob / NewPoolJob(name, job) |
Pairs a name with a workergroup.Job for RunWorkerPool |
WorkerResult |
Per-task outcome: Name(), Index(), Value(), Cause(), Duration(), IsSuccess() |
RunWorkerGroup(ctx, concurrency, tasks []*WorkerTask) *wrapper |
Run a fixed batch of named tasks concurrently; return the aggregated *wrapper |
RunWorkerPool(ctx, workers, jobs []*PoolJob) *wrapper |
Run a fixed batch of jobs through a worker pool; return the aggregated *wrapper |
WorkerGroup / NewWorkerGroupWithContext(ctx, opts...) |
Lower-level builder for incremental submission (Go, TryGo, Wait) |
Pool / NewPool(ctx, initialWorkers, opts...) |
Lower-level builder for a long-lived, dynamically scalable worker pool |
Status mapping
Wait() and the Run* helpers always resolve to exactly one outcome:
| Scenario | Status Code | Notes |
|---|---|---|
| Every task/job succeeded | 200 OK |
— |
| Some succeeded, some failed | 207 MultiStatus |
No top-level error; inspect each WorkerResult.Cause() |
| Every task/job failed | 500 InternalServerError |
Combined error attached via WithErrorAck (errors.Is/As work) |
| Caller's Context was canceled | 499 ClientClosedRequest |
Detected independently of task errors |
| Caller's Context deadline exceeded | 504 GatewayTimeout |
Same, for context.DeadlineExceeded |
| No tasks/jobs submitted | 204 NoContent |
— |
The full, ordered []*WorkerResult is always available via wrapper.Body(); no per-task outcome is discarded.
Quick start: fan out a batch of named tasks
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/polarixa/replify"
)
func main() {
ctx := context.Background()
w := replify.RunWorkerGroup(ctx, 4, []*replify.WorkerTask{
replify.NewWorkerTask("users", func(ctx context.Context) (any, error) { return []string{"alice"}, nil }),
replify.NewWorkerTask("orders", func(ctx context.Context) (any, error) { return nil, errors.New("orders unavailable") }),
replify.NewWorkerTask("invoices", func(ctx context.Context) (any, error) { return 3, nil }),
})
switch w.StatusCode() {
case replify.StatusOK.Value():
fmt.Println("every fetch succeeded")
case replify.StatusMultiStatus.Value():
results, ok := w.Body().([]*replify.WorkerResult)
if !ok {
log.Fatal("unexpected body type")
}
for _, r := range results {
if !r.IsSuccess() {
log.Printf("%s failed: %v", r.Name(), r.Cause()) // orders failed: orders unavailable
}
}
}
fmt.Println(w.StatusCode()) // 207
}Quick start: persistent worker pool for background jobs
package main
import (
"context"
"github.com/polarixa/replify"
)
func main() {
ctx := context.Background()
w := replify.RunWorkerPool(ctx, 8, []*replify.PoolJob{
replify.NewPoolJob("email-1", func(ctx context.Context) error { return nil }),
replify.NewPoolJob("email-2", func(ctx context.Context) error { return nil }),
replify.NewPoolJob("email-3", func(ctx context.Context) error { return nil }),
})
w.Slogging() // logs the aggregated outcome in one line (status 200)
}Quick start: incremental submission with the lower-level builders
Use WorkerGroup/Pool directly when tasks are not known upfront (e.g. generated while reading a channel) or when a pool outlives a single request.
package main
import (
"context"
"fmt"
"log"
"github.com/polarixa/replify"
"github.com/polarixa/replify/pkg/workergroup"
)
func main() {
ctx := context.Background()
g, gctx := replify.NewWorkerGroupWithContext(ctx, workergroup.WithLimit(4))
for _, id := range []int{1, 2, 3} {
g.Go(fmt.Sprintf("user-%d", id), func(ctx context.Context) (any, error) {
_ = gctx // group-derived context, canceled when the group fails in FirstError mode
return id * 10, nil
})
}
fmt.Println(g.Wait().StatusCode()) // 200
// A pool that lives for the lifetime of the process, scaled on demand.
pool := replify.NewPool(ctx, 4)
defer pool.Close()
if err := pool.ScaleTo(16); err != nil { // safe even while jobs are running
log.Fatal(err)
}
if err := pool.Submit(ctx, "resize-thumbnail", func(ctx context.Context) error { return nil }); err != nil {
log.Fatal(err)
}
}Real-world use cases: mapping to APIs
| Use case | Recommended API |
|---|---|
| Dashboard/BFF endpoint aggregating N downstream microservice calls | RunWorkerGroup: bounded concurrency, partial failure visible as 207 |
| Health check endpoint pinging several dependencies | RunWorkerGroup: one failed dependency doesn't hide the others' status |
| Bulk batch processing (CSV import, bulk email, bulk notifications) | RunWorkerGroup or RunWorkerPool: per-item success/failure tracked individually |
| Background job queue (order processing, webhook delivery, thumbnails) | Pool: long-lived, independently scalable workers consuming a queue |
| Request-scoped parallel I/O (DB + cache + external API in one handler) | RunWorkerGroup with a request-bound context.Context for automatic cancellation |
| Scheduled/cron-triggered fan-out (nightly reconciliation across accounts) | RunWorkerGroup with concurrency tuned to the downstream rate limit |
| Rate-limited work against a fragile downstream (payment gateway, SMS provider) | WorkerGroup/Pool with workergroup.WithLimit / ScaleTo |
| A single job that must not take down a whole worker loop | workergroup.WithPanicRecovery / WithPoolPanicRecovery; see ErrWorkerPanicked |
| Sequential execution that still benefits from the aggregated wrapper | RunWorkerGroup(ctx, 1, tasks): same result shape, no concurrency |
Use case 1: Dashboard/BFF aggregating downstream calls
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/polarixa/replify"
)
func DashboardHandler(w http.ResponseWriter, r *http.Request) {
result := replify.RunWorkerGroup(r.Context(), 4, []*replify.WorkerTask{
replify.NewWorkerTask("profile", func(ctx context.Context) (any, error) { return map[string]string{"name": "Alice"}, nil }),
replify.NewWorkerTask("orders", func(ctx context.Context) (any, error) { return []int{1, 2}, nil }),
replify.NewWorkerTask("notifications", func(ctx context.Context) (any, error) { return nil, errors.New("timeout") }),
})
results, ok := result.Body().([]*replify.WorkerResult)
if !ok {
log.Println("unexpected body type")
return
}
// A 207 means the dashboard is still usable, just missing a widget.
dashboard := map[string]any{}
for _, res := range results {
if res.IsSuccess() {
dashboard[res.Name()] = res.Value()
}
}
result.WithBody(dashboard).Write(w)
}
func main() {
rec := httptest.NewRecorder()
DashboardHandler(rec, httptest.NewRequest(http.MethodGet, "/dashboard", nil))
fmt.Println(rec.Code) // 207
}Use case 2: Health check pinging dependencies
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/polarixa/replify"
)
func HealthzHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
// 200 when all are healthy, 207 when some are degraded, 500 when all are down.
replify.RunWorkerGroup(ctx, 0, []*replify.WorkerTask{
replify.NewWorkerTask("database", func(ctx context.Context) (any, error) { return nil, nil }),
replify.NewWorkerTask("cache", func(ctx context.Context) (any, error) { return nil, nil }),
replify.NewWorkerTask("message-queue", func(ctx context.Context) (any, error) { return nil, nil }),
}).Write(w)
}
func main() {
rec := httptest.NewRecorder()
HealthzHandler(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
fmt.Println(rec.Code) // 200
}Use case 3: Bulk batch processing
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/polarixa/replify"
)
type UserRow struct{ Email string }
func upsertUser(ctx context.Context, row UserRow) error {
if row.Email == "bad@example.com" {
return errors.New("constraint violation")
}
return nil
}
func main() {
rows := []UserRow{{"a@example.com"}, {"bad@example.com"}, {"c@example.com"}}
var tasks []*replify.WorkerTask
for _, row := range rows {
tasks = append(tasks, replify.NewWorkerTask(row.Email, func(ctx context.Context) (any, error) {
return nil, upsertUser(ctx, row)
}))
}
// Bound concurrency to 10 so the import cannot overwhelm the database pool.
result := replify.RunWorkerGroup(context.Background(), 10, tasks)
results, ok := result.Body().([]*replify.WorkerResult)
if !ok {
log.Fatal("unexpected body type")
}
var failedEmails []string
for _, r := range results {
if !r.IsSuccess() {
failedEmails = append(failedEmails, r.Name())
}
}
result.WithDebuggingKV("failed_emails", failedEmails).Slogging()
fmt.Println(failedEmails) // [bad@example.com]
}Use case 4: Background job queue with a scalable pool
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/polarixa/replify"
)
// Created once at startup; lives for the process lifetime.
var jobPool = replify.NewPool(context.Background(), 8)
func EnqueueOrderHandler(w http.ResponseWriter, r *http.Request) {
orderID := r.PathValue("id")
err := jobPool.Submit(r.Context(), "process-order-"+orderID, func(ctx context.Context) error {
return nil // processOrder(ctx, orderID)
})
if err != nil {
replify.New().ServiceUnavailable().WithErrorAck(err).Write(w)
return
}
replify.New().Accepted().WithMessage("order queued for processing").Write(w)
}
// ScaleOrderWorkers is safe to call while earlier jobs are still running.
func ScaleOrderWorkers(n int) error { return jobPool.ScaleTo(n) }
func GracefulShutdown() {
jobPool.Close() // stop accepting new jobs
jobPool.Wait().Slogging() // wait for the queue to drain, log the outcome
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /orders/{id}", EnqueueOrderHandler)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/orders/42", nil))
fmt.Println(rec.Code) // 202
if err := ScaleOrderWorkers(16); err != nil {
fmt.Println(err)
}
GracefulShutdown()
}Use case 5: Request-scoped parallel I/O
r.Context() is canceled when the client disconnects; RunWorkerGroup surfaces that as 499 Client Closed Request.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/polarixa/replify"
)
func GetUserProfileHandler(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
replify.RunWorkerGroup(r.Context(), 0, []*replify.WorkerTask{
replify.NewWorkerTask("db", func(ctx context.Context) (any, error) { return map[string]string{"id": userID}, nil }),
replify.NewWorkerTask("cache-warm", func(ctx context.Context) (any, error) { return nil, nil }),
replify.NewWorkerTask("recommendations-api", func(ctx context.Context) (any, error) { return []string{"x"}, nil }),
}).Write(w)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", GetUserProfileHandler)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/users/7", nil))
fmt.Println(rec.Code) // 200
}Use case 6: Scheduled fan-out (nightly reconciliation)
package main
import (
"context"
"errors"
"log"
"github.com/polarixa/replify"
)
func reconcileAccount(ctx context.Context, id string) error {
if id == "acct-2" {
return errors.New("ledger mismatch")
}
return nil
}
func NightlyReconciliation(ctx context.Context, accountIDs []string) {
var tasks []*replify.WorkerTask
for _, id := range accountIDs {
tasks = append(tasks, replify.NewWorkerTask(id, func(ctx context.Context) (any, error) {
return nil, reconcileAccount(ctx, id)
}))
}
// Keep concurrency within the ledger service's documented rate limit.
result := replify.RunWorkerGroup(ctx, 5, tasks)
result.WithDebuggingKV("accounts_total", len(accountIDs)).Slogging()
if result.StatusCode() == replify.StatusMultiStatus.Value() {
results, ok := result.Body().([]*replify.WorkerResult)
if !ok {
return
}
for _, r := range results {
if !r.IsSuccess() {
log.Printf("reconciliation failed for account %s: %v", r.Name(), r.Cause()) // account acct-2: ledger mismatch
}
}
}
}
func main() {
NightlyReconciliation(context.Background(), []string{"acct-1", "acct-2", "acct-3"})
}Use case 7: Rate-limited downstream with live throttling
package main
import (
"context"
"fmt"
"github.com/polarixa/replify"
"github.com/polarixa/replify/pkg/workergroup"
)
func main() {
g, _ := replify.NewWorkerGroupWithContext(context.Background(), workergroup.WithLimit(5))
for _, phone := range []string{"+100", "+101", "+102"} {
g.Go(phone, func(ctx context.Context) (any, error) {
return nil, nil // smsProvider.Send(ctx, phone, message)
})
}
// SetLimit is promoted from the embedded *workergroup.Group and is safe while goroutines are active.
g.SetLimit(1)
w := g.Wait()
w.Slogging()
fmt.Println(w.StatusCode()) // 200
}Use case 8: Panic isolation in a worker loop
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/polarixa/replify"
"github.com/polarixa/replify/pkg/workergroup"
)
func main() {
ctx := context.Background()
panicRecovery := workergroup.WithPoolPanicRecovery(func(recovered any, stack []byte) {
log.Printf("render job panicked: %v\n%s", recovered, stack)
})
pool := replify.NewPool(ctx, 4, panicRecovery)
defer pool.Close()
if err := pool.Submit(ctx, "render-1", func(ctx context.Context) error {
panic("renderer bug") // must not take down the other workers
}); err != nil {
log.Fatal(err)
}
if err := pool.Submit(ctx, "render-2", func(ctx context.Context) error { return nil }); err != nil {
log.Fatal(err)
}
result := pool.Wait()
results, ok := result.Body().([]*replify.WorkerResult)
if !ok {
log.Fatal("unexpected body type")
}
for _, r := range results {
if errors.Is(r.Cause(), replify.ErrWorkerPanicked) {
fmt.Printf("job %q recovered from a panic and was marked failed\n", r.Name()) // job "render-1" ...
}
}
}Use case 9: Sequential pipeline with aggregated wrapper
package main
import (
"context"
"log"
"github.com/polarixa/replify"
)
func main() {
// concurrency == 1 guarantees backup -> migrate -> verify order (Index matches call order).
result := replify.RunWorkerGroup(context.Background(), 1, []*replify.WorkerTask{
replify.NewWorkerTask("backup", func(ctx context.Context) (any, error) { return nil, nil }),
replify.NewWorkerTask("migrate", func(ctx context.Context) (any, error) { return nil, nil }),
replify.NewWorkerTask("verify", func(ctx context.Context) (any, error) { return nil, nil }),
})
if result.StatusCode() != replify.StatusOK.Value() {
log.Fatalf("migration pipeline failed: %s", result.JSON())
}
log.Println("pipeline ok") // pipeline ok
}When to use / when NOT to use WorkerGroup
Use when:
- You fan out to multiple independent operations and need one HTTP-shaped response distinguishing all succeeded / some failed / all failed.
- You need per-task identity (
WorkerResult.Name()), duration, and the original unwrapped error forerrors.Is/errors.As. - You need a persistent pool that scales live (
ScaleTo/ScaleBy), without theerrgrouprestriction against reconfiguring while goroutines are active. - Cancellation or deadlines must yield a distinguishable status (
499/504) instead of a generic task error. - Concurrent results should flow into the same logging (
Logging/Slogging), serialization (JSON/Respond), and HTTP-writing (Write) pipeline.
Do not use when:
- A single synchronous call: wrapping one call adds goroutine, mutex, and allocation overhead for no benefit.
- Tight, CPU-bound inner loops where per-task bookkeeping (
WorkerResultallocation, mutex-protected append) is measurable; usepkg/workergroupdirectly orsync.WaitGroup. - Strict completion-order guarantees:
[]*WorkerResultis ordered by the order tasks began running, matching submission order only whenconcurrency == 1. - Streaming partial results to a client:
Wait()blocks until all tasks finish; usepkg/workergroupdirectly orPool.Submitfrom multiple goroutines and stream yourself. - Extremely hot, latency-sensitive paths where every allocation is budgeted; prefer
pkg/workergroup'sGroup/WorkerPoolwithout the result-tracking layer. - Non-idempotent, exactly-once semantics:
workergroup.CollectErrors(the default forRunWorkerGroup/RunWorkerPool) keeps running remaining tasks after a failure; useworkergroup.WithErrorMode(workergroup.FirstError)via the lower-level builders when a failure must stop everything.
API / Configuration Reference
Standard response format and field descriptions
| Field | Type | Description |
|---|---|---|
data |
interface{} |
The primary data payload of the response |
status_code |
int |
HTTP status code for the response |
message |
string |
Human-readable message providing context |
total |
int |
Total number of items (used in non-paginated responses) |
path |
string |
Request path for which the response is generated |
meta |
object |
Metadata about the API response |
meta.request_id |
string |
Unique identifier for the request, useful for debugging |
meta.api_version |
string |
API version used for the request |
meta.locale |
string |
Locale used for the request (e.g., en_US) |
meta.requested_time |
string |
Timestamp when the request was made (ISO 8601) |
meta.custom_fields |
object |
Additional custom metadata fields |
pagination |
object |
Pagination details, if applicable |
pagination.page |
int |
Current page number |
pagination.per_page |
int |
Number of items per page |
pagination.total_items |
int |
Total number of items available |
pagination.total_pages |
int |
Total number of pages |
pagination.is_last |
bool |
Whether this is the last page |
debug |
object |
Debugging information (useful for development) |
Wrapper type and core functions
type R struct {
*wrapper
}R is a high-level abstraction providing a simplified interface for handling API responses.
| Function | Description |
|---|---|
New() *wrapper |
Creates a new response wrapper |
Pages() *pagination |
Creates a new pagination object |
UnwrapJSON(jsonStr string) (*wrapper, error) |
Parses a JSON string into a wrapper |
Response configuration methods
| Method | Description |
|---|---|
WithStatusCode(code int) |
Sets HTTP status code |
WithBody(v interface{}) |
Sets response body/data |
WithMessage(message string) |
Sets response message |
WithMessagef(format string, args...) |
Sets formatted message |
WithError(message string) |
Sets error message |
WithErrorf(format string, args...) |
Sets formatted error |
WithErrorAck(err error) |
Sets error with stack trace |
AppendError(err error, message string) |
Wraps error with context |
AppendErrorf(err error, format string, args...) |
Wraps error with formatted context |
WithPath(v string) |
Sets request path |
WithPathf(v string, args...) |
Sets formatted request path |
WithTotal(total int) |
Sets total items count |
Metadata methods
| Method | Description |
|---|---|
WithRequestID(v string) |
Sets request ID |
WithRequestIDf(format string, args...) |
Sets formatted request ID |
WithApiVersion(v string) |
Sets API version |
WithApiVersionf(format string, args...) |
Sets formatted API version |
WithLocale(v string) |
Sets locale (e.g., en_US) |
WithRequestedTime(v time.Time) |
Sets request timestamp |
WithCustomFieldKV(key string, value interface{}) |
Adds custom metadata field |
WithCustomFieldKVf(key, format string, args...) |
Adds formatted custom field |
WithCustomFields(values map[string]interface{}) |
Sets multiple custom fields |
WithMeta(v *meta) |
Sets entire metadata object |
WithHeader(v *header) |
Sets the header |
Pagination methods
| Method | Description |
|---|---|
WithPagination(v *pagination) |
Sets pagination object |
WithPage(v int) |
Sets current page number |
WithPerPage(v int) |
Sets items per page |
WithTotalItems(v int) |
Sets total items count |
WithTotalPages(v int) |
Sets total pages count |
WithIsLast(v bool) |
Sets whether the current page is last |
Debugging methods
| Method | Description |
|---|---|
WithDebugging(v map[string]interface{}) |
Sets debug information map |
WithDebuggingKV(key string, value interface{}) |
Adds single debug key-value |
WithDebuggingKVf(key, format string, args...) |
Adds formatted debug value |
Write methods
| Method | Description |
|---|---|
Write(w http.ResponseWriter) |
Dispatches to WriteFile, WriteBinary, or WriteJSON based on wrapper state |
WriteJSON / WriteFile / WriteBinary |
Dedicated terminal writers |
File(path) |
Sets the file path to serve |
FileAttachment(path, name) |
Serves a file with Content-Disposition: attachment |
Binary(data []byte) |
Sets a binary payload |
Filename(name) |
Sets the filename driving MIME detection and Content-Disposition |
Slogging() / S() |
Structured logging of the wrapper / access to the logger |
Query methods
| Method | Returns | Description |
|---|---|---|
Available() |
bool |
Checks if wrapper is non-nil |
StatusCode() |
int |
Gets HTTP status code |
StatusText() |
string |
Gets status text (e.g., "OK") |
Body() |
interface{} |
Gets response body |
Message() |
string |
Gets response message |
Error() |
string |
Gets error message |
Cause() |
error |
Gets underlying error cause |
Total() |
int |
Gets total items |
Meta() |
*meta |
Gets metadata object |
Header() |
*header |
Gets header object |
Pagination() |
*pagination |
Gets pagination object |
Debugging() |
map[string]interface{} |
Gets debug information |
OnDebugging(key string) |
interface{} |
Gets specific debug value |
Conditional check methods
| Method | Returns | Description |
|---|---|---|
IsSuccess() |
bool |
Status is 2xx |
IsClientError() |
bool |
Status is 4xx |
IsServerError() |
bool |
Status is 5xx |
IsRedirection() |
bool |
Status is 3xx |
IsError() |
bool |
Error exists or status is 4xx/5xx |
IsErrorPresent() |
bool |
Error field exists |
IsBodyPresent() |
bool |
Body exists |
IsPagingPresent() |
bool |
Pagination exists |
IsMetaPresent() |
bool |
Metadata exists |
IsHeaderPresent() |
bool |
Header exists |
IsDebuggingPresent() |
bool |
Debug info exists |
IsDebuggingKeyPresent(key string) |
bool |
Specific debug key exists |
IsLastPage() |
bool |
Current page is last |
IsStatusCodePresent() |
bool |
Valid status code exists |
IsTotalPresent() |
bool |
Total count exists |
Serialization methods
| Method | Returns | Description |
|---|---|---|
JSON() |
string |
Compact JSON string |
JSONPretty() |
string |
Pretty-printed JSON |
Respond() |
map[string]interface{} |
Map representation |
Reply() |
R |
Returns R wrapper |
JSON body (fj) methods on the wrapper
| Method | Description |
|---|---|
QueryJSONBody(path) / QueryJSONBodyMulti(paths...) |
Single / multi-path query |
JSONBodyParser() |
Parse once into an fj.Context for repeated queries |
ValidJSONBody() |
Validate the body as JSON |
SumJSONBody / MinJSONBody / MaxJSONBody / AvgJSONBody |
Numeric aggregates over a path |
SearchJSONBody / SearchJSONBodyMatch |
Substring / wildcard search over leaf values |
SearchJSONBodyByKey / SearchJSONBodyByKeyPattern |
Search by key name / wildcard key pattern |
JSONBodyContains / JSONBodyContainsMatch |
Substring / wildcard check at a path |
FindJSONBodyPath / FindJSONBodyPathsMatch |
Locate dot-notation path(s) of a value / pattern |
CountJSONBody / FilterJSONBody / FirstJSONBody |
Count, filter by predicate, first match |
DistinctJSONBody / PluckJSONBody / GroupByJSONBody / SortJSONBody |
Dedupe, project, group, sort |
Execution (WorkerGroup / Pool) API
See the core types table and status mapping above. Related options: workergroup.WithLimit, workergroup.WithErrorMode (CollectErrors default, FirstError), workergroup.WithPanicRecovery, workergroup.WithPoolPanicRecovery; pool methods Submit, ScaleTo, ScaleBy, Close, Wait; sentinel ErrWorkerPanicked.
Configuration and environment
replify has no required configuration files or environment variables. Defaults applied by New(): api_version v0.0.1, locale en_US, generated request_id, and requested_time. The only environment variable referenced in the documentation patterns is application-defined (ENV=development) for gating debug output.
HTTP status codes reference
Full reference: docs/HTTP_STATUS_CODES_REFERENCE.md.
Common API scenarios
| Scenario | HTTP Status Codes | Example |
|---|---|---|
| Successful resource retrieval | 200 OK, 304 Not Modified | GET /users/123 returns user data |
| Resource creation | 201 Created | POST /users creates a new user |
| Asynchronous processing | 202 Accepted | POST /large-file starts a file upload |
| Validation errors | 400 Bad Request | POST /users missing a required field |
| Authentication issues | 401 Unauthorized, 403 Forbidden | Invalid credentials or permissions |
| Rate limiting | 429 Too Many Requests | Exceeded API request limits |
| Missing resource | 404 Not Found | GET /users/999 user not found |
| Server failures | 500 Internal Server Error, 503 Service Unavailable | Database failure or maintenance |
| Version conflicts | 409 Conflict | Outdated version causing conflict |
Success (2xx)
| Code | Status | Use Case |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (resource created) |
| 202 | Accepted | Async processing started |
| 204 | No Content | Successful DELETE |
| 206 | Partial Content | Video streaming, range requests |
Redirection (3xx)
| Code | Status | Use Case |
|---|---|---|
| 301 | Moved Permanently | Resource permanently moved |
| 302 | Found | Temporary redirect |
| 304 | Not Modified | Cached content still valid |
| 307 | Temporary Redirect | POST redirect maintaining method |
| 308 | Permanent Redirect | Permanent redirect maintaining method |
Client errors (4xx)
| Code | Status | Use Case |
|---|---|---|
| 400 | Bad Request | Invalid request format/data |
| 401 | Unauthorized | Missing/invalid authentication |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource conflict (duplicate) |
| 413 | Payload Too Large | Request body too large |
| 415 | Unsupported Media Type | Invalid content type |
| 422 | Unprocessable Entity | Validation errors |
| 429 | Too Many Requests | Rate limiting |
Server errors (5xx)
| Code | Status | Use Case |
|---|---|---|
| 500 | Internal Server Error | Unexpected server error |
| 501 | Not Implemented | Feature not implemented |
| 502 | Bad Gateway | Upstream service error |
| 503 | Service Unavailable | Service down/maintenance |
| 504 | Gateway Timeout | Upstream timeout |
Best Practices
Do
- Always set status codes.
- Use request IDs for tracing (
WithRequestID(r.Header.Get("X-Request-ID"))). - Include the API version (
WithApiVersion("v1.0.0")). - Use
WithErrorAckfor stack traces. - Check response status before processing (
if response.IsSuccess() { ... }). - Use pagination for list endpoints.
- Check existence before using an
fjvalue (ctx.Exists()); parse once, query many (JSONBodyParser()); preferGetBytesfor byte-slice payloads; validate untrusted input first (ValidJSONBody()); register custom transformers ininit(); never mutateUnsafeBytesoutput.
Code: recommended patterns
package main
import (
"errors"
"fmt"
"github.com/polarixa/replify"
"github.com/polarixa/replify/pkg/fj"
)
func main() {
data := map[string]string{"k": "v"}
ok := replify.New().
WithStatusCode(200).
WithRequestID("req-1").
WithApiVersion("v1.0.0").
WithBody(data)
if ok.IsSuccess() {
fmt.Println(ok.Body()) // map[k:v]
}
failed := replify.New().WithStatusCode(500).WithErrorAck(errors.New("boom"))
fmt.Println(failed.IsServerError()) // true
if !ok.ValidJSONBody() {
fmt.Println("invalid JSON body")
return
}
if ctx := ok.QueryJSONBody("optional.key"); ctx.Exists() {
fmt.Println(ctx.String())
}
rawBytes := []byte(`{"user":{"name":"Alice"}}`)
fmt.Println(fj.GetBytes(rawBytes, "user.name").String()) // Alice (no string conversion allocation)
doc := ok.JSONBodyParser()
fmt.Println(doc.Get("k").String()) // v
}Don't
- Don't forget status codes:
replify.New().WithBody(data)leaves the status unset. - Don't expose sensitive debug info in production: gate
WithDebuggingKVbehindos.Getenv("ENV") == "development"; never log secrets such as database passwords. - Don't use generic error messages: prefer
"Failed to create user: email already exists"over"Error occurred". - Don't ignore error checking on
UnwrapJSON.
Code: anti-patterns and their fixes
package main
import (
"fmt"
"log"
"os"
"github.com/polarixa/replify"
)
func main() {
data := "payload"
// Bad: replify.New().WithBody(data)
good := replify.New().WithStatusCode(200).WithBody(data)
if os.Getenv("ENV") == "development" {
good.WithDebuggingKV("query", "SELECT 1")
}
jsonStr := `{"status_code":200}`
wrapper, err := replify.UnwrapJSON(jsonStr)
if err != nil {
log.Printf("Failed to parse JSON: %v", err)
return
}
fmt.Println(wrapper.StatusCode()) // 200
_ = replify.New().WithError("Failed to create user: email already exists")
}Architecture Notes
Response construction and write dispatch
handler
└─ replify.New() -> *wrapper (decorator target)
├─ With* builders -> mutate state fluently (body, meta, pagination, debug, error, ...)
└─ Write(http.ResponseWriter)
├─ filepath set ─────────────> WriteFile (mime detection, Content-Disposition via RFC 5987)
├─ data is []byte ────────────> WriteBinary
└─ otherwise ─────────────────> WriteJSON (status from wrapper, application/json)
fj query path
HTTP Request -> wrapper.WithBody(data) -> wrapper.QueryJSONBody("user.name")
|
v
fj.Get(jsonString, "user.name")
|
v
fj.Context (single value, no full decode)
QueryJSONBody serializes the body per call; JSONBodyParser() amortizes that cost.
WorkerGroup / Pool outcome resolution
flowchart TD
A[Wait / RunWorker*] --> B{ctx.Err?}
B -- Canceled --> C[499 Client Closed Request]
B -- DeadlineExceeded --> D[504 Gateway Timeout]
B -- nil --> E{tasks submitted?}
E -- none --> F[204 No Content]
E -- some --> G{failures?}
G -- none --> H[200 OK]
G -- partial --> I[207 Multi-Status]
G -- all --> J[500 + combined error]
Per-task WorkerResult entries (ordered by start) are always attached as the wrapper body.
Repository layout
- Root package: wrapper, headers, meta, pagination, cursor, links, reason, issue, signature, writers, middleware, error stacks, worker integration.
cmd/replify: CLI entry point;main/: development entry point.pkg/:assert,b64,codegen,coll,common,conv,crontask,encoding,explorer,fj,hashy,match,msort,netx,randn,ref,slogger,strchain,strutil,sysx,translit,truncate,workergroup.docs/: HTTP status code reference and release notes.
Testing & Benchmarks
Running tests
go test ./... # all packages
go test -count=1 -cover ./... # no cache, with coverage (same as `make test`)
# ok github.com/polarixa/replify 0.xxxs coverage: xx.x% of statementsMakefile targets: dev, run, build, test, tidy, deps-upgrade, deps-clean-cache, clean, tree, tree-dirs.
Mocking strategy
The library is dependency-free and built on http.ResponseWriter, so tests need no mocking framework: use net/http/httptest (httptest.NewRecorder, httptest.NewRequest) to exercise writers, middleware, and handlers, as in the usage examples above.
Benchmarks
Benchmarks exist in pkg/codegen, pkg/hashy, and pkg/slogger. No published results are bundled; run them locally:
go test -run '^$' -bench . -benchmem ./pkg/...
# BenchmarkXxx-N <iterations> <ns/op> <B/op> <allocs/op>Contributing
Workflow
- Clone the repository
- Navigate to the project directory
- Prepare the environment
- Make your changes: follow Go best practices, add tests for new features, update documentation.
- Run tests
- Submit a pull request
git clone --depth 1 https://github.com/polarixa/replify.git
cd replify
go mod tidy
# ... make changes ...
go test ./...
# ok github.com/polarixa/replify ...Contribution terms and support
Pull Requests are welcome. By contributing to replify, you agree that your contributions will be licensed under the project's Apache License 2.0 and you grant polarixa the rights to use and distribute them as part of the ecosystem.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
License
Licensed under the Apache License 2.0; see LICENSE.
Enterprise and commercial use: replify is designed to be safely integrated into commercial, closed-source, and SaaS applications. The Apache 2.0 license provides an explicit patent grant, ensuring legal safety for enterprise adoption.
Built with ❤️ for the Go community.
{ "status_code": 200, "message": "Resource retrieved successfully", "path": "/api/v1/users", "data": [ // abstract data (can be array or object) { "id": "user_01J6G7W9K2M4X7V5P8B3Q2Z1NS", "username": "jdoe_dev", "email": "j.doe@example.com", "role": "administrator", "status": "active", "created_at": "2025-01-15T08:30:00Z", "last_login": "2026-02-26T14:15:22Z", }, { "id": "user_01J6G7W9K2M4X7V5P8B3Q2Z1NT", "username": "s_smith", "email": "sarah.smith@example.com", "role": "editor", "status": "active", "created_at": "2025-02-01T10:15:00Z", "last_login": "2026-02-25T09:45:10Z", }, ], "pagination": { "page": 1, "per_page": 2, "total_items": 120, "total_pages": 60, "is_last": false, }, "cursor": { "next": "eyJsYXN0X2lkIjoiMTAwMSJ9", "previous": "eyJsYabc...", "has_next": true, "has_previous": false, "limit": 20, }, "meta": { "request_id": "req_80eafc6a1655ec5a06595d155f1e6951", "api_version": "v1.0.4", "locale": "en_US", "requested_time": "2026-02-26T17:30:28.983Z", "custom_fields": { // custom fields "trace_id": "80eafc6a1655ec5a06595d155f1e6951", "origin_region": "us-east-1", }, }, "header": { "code": 500, "text": "Internal Server Error", "type": "Server Error", }, "issue": { "fingerprint": "IFP-77B446", "id": "ISS-1A91A217", "message": "panic: intentional panic - testing Recovery middleware", }, "debug": { // custom fields "trace_session_id": "4919e84fc26881e9fe790f5d07465db4", "execution_time_ms": 42, }, "signature": { "algorithm": "HMAC-SHA512", "timestamp": 1790513074, "value": "MhDQFzogLmHjVzHGMeGo7Km8OMlW2HfMhT4swSto9o7/KLTvIHtXtPbMA+rUpQ49INwOK2gDGTPRZwKOhPkQoA==", }, "reason": { "category": "VALIDATION", "code": "FIELD_INVALID" }, "_links": { "next": { "href": "/api/v1/users?page=2", "method": "GET" }, "self": { "href": "http://localhost:8080/api/v1/users", "method": "GET" }, }, }