replify is a Go library designed to simplify and standardize API response wrapping for RESTful services. It leverages the Decorator Pattern to dynamically add error handling, metadata, pagination, and other response features in a clean and human-readable format.
Building RESTful APIs often requires repetitive boilerplate code for standardizing responses. replify eliminates this by providing a fluent, chainable API that ensures consistent response formats across all your endpoints.
- ❌ Inconsistent response formats across different endpoints
- ❌ Repetitive error handling boilerplate in every handler
- ❌ Manual metadata management (request IDs, timestamps, versions)
- ❌ Complex pagination logic scattered throughout the codebase
- ❌ Debugging difficulties in production vs development environments
✅ Standardized response structure - One format for all endpoints
✅ Fluent API - Chainable methods for building responses
✅ Built-in pagination - Complete pagination support out of the box
✅ Metadata management - Request IDs, timestamps, API versions, locales
✅ Conditional debugging - Development-only debug information
✅ Error handling - Stack traces, error wrapping, contextual messages
✅ Type safety - Full type safety with Go generics
✅ Zero dependencies - Only uses Go standard library
- 🎯 Standardized JSON Format - Consistent structure across all API responses
- 🔗 Fluent Builder Pattern - Chain methods to construct complex responses
- 📄 Pagination Support - Built-in page, per_page, total_items, total_pages, is_last
- 🔍 Request Tracing - Track requests with unique 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
- 📊 Metadata - API version, custom fields, timestamps
- ✅ Status Helpers - IsSuccess(), IsClientError(), IsServerError()
- 🔄 JSON Parsing - Parse JSON strings back to wrapper objects
- Go version 1.23 or higher
# Latest version
go get -u github.com/sivaosorg/replify@latest
# Specific version
go get github.com/sivaosorg/replify@v0.0.1import "github.com/sivaosorg/replify"With Go's module support, go [build|run|test] automatically fetches the necessary dependencies when you add the import.
package main
import (
"fmt"
"github.com/sivaosorg/replify"
)
func main() {
// Create a simple success response
response := replify.New().
WithStatusCode(200).
WithMessage("User retrieved successfully").
WithBody(map[string]string{
"id": "123",
"name": "John Doe",
})
fmt.Println(response.JsonPretty())
}Output:
{
"data": {
"id": "123",
"name": "John Doe"
},
"headers": {
"code": 200,
"text": "OK"
},
"message": "User retrieved successfully",
"meta": {
"api_version": "v0.0.1",
"locale": "en_US",
"request_id": "d7e5ce24b796da94770911db36565bf9",
"requested_time": "2026-01-29T10:07:05.751501+07:00"
},
"status_code": 200,
"total": 0
}The library produces responses in this standardized format:
{
"data": "response body here",
"status_code": 200,
"message": "How are you? I'm good",
"total": 1,
"path": "/api/v1/users",
"meta": {
"request_id": "80eafc6a1655ec5a06595d155f1e6951",
"api_version": "v0.0.1",
"locale": "en_US",
"requested_time": "2024-12-14T20:24:23.983839+07:00",
"custom_fields": {
"fields": "userID: 103"
}
},
"pagination": {
"page": 1000,
"per_page": 2,
"total_items": 120,
"total_pages": 34,
"is_last": true
},
"debug": {
"___abc": "trace sessions_id: 4919e84fc26881e9fe790f5d07465db4",
"refer": 1234
}
}| 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 |
Indicates whether this is the last page |
debug |
object |
Debugging information (useful for development) |
response := replify.New().
WithStatusCode(200).
WithMessage("Operation successful").
WithBody(data)response := replify.New().
WithStatusCode(400).
WithError("Invalid input: email is required").
WithMessage("Validation failed")response := replify.New().
WithStatusCode(200).
WithBody(users).
WithRequestID("req-123-456").
WithApiVersion("v1.0.0").
WithLocale("en_US").
WithPath("/api/v1/users")pagination := replify.Pages().
WithPage(1).
WithPerPage(20).
WithTotalItems(150).
WithTotalPages(8).
WithIsLast(false)
response := replify.New().
WithStatusCode(200).
WithBody(users).
WithPagination(pagination).
WithTotal(20)response := replify.New().
WithStatusCode(500).
WithError("Database connection failed").
WithDebuggingKV("query", "SELECT * FROM users").
WithDebuggingKV("error_code", "CONN_TIMEOUT").
WithDebuggingKV("retry_count", 3)package main
import (
"fmt"
"github.com/sivaosorg/replify"
"github.com/sivaosorg/replify/pkg/randn"
)
func main() {
// Create pagination
p := replify.Pages().
WithIsLast(true).
WithPage(1000).
WithTotalItems(120).
WithTotalPages(34).
WithPerPage(2)
// Create response
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
}
// Access response properties
fmt.Println(w.Json())
fmt.Println(w.StatusCode())
fmt.Println(w.StatusText())
fmt.Println(w.Message())
fmt.Println(w.Body())
fmt.Println(w.IsSuccess())
fmt.Println(w.Respond())
// Check metadata
fmt.Println(w.Meta().IsCustomFieldPresent())
fmt.Println(w.Meta().IsApiVersionPresent())
fmt.Println(w.Meta().IsRequestIDPresent())
fmt.Println(w.Meta().IsRequestedTimePresent())
}package main
import (
"fmt"
"log"
"time"
"github.com/sivaosorg/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())
fmt.Printf("%+v\n", w.OnKeyDebugging("___abc"))
fmt.Printf("%+v\n", w.JsonPretty())
}package main
import (
"encoding/json"
"net/http"
"github.com/sivaosorg/replify"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// GET /users/:id
func GetUser(w http.ResponseWriter, r *http.Request) {
id := getIDFromPath(r)
user, err := findUserByID(id)
var response *replify.R
if err != nil {
response = replify.New().
WithStatusCode(404).
WithError(err.Error()).
WithMessage("User not found").
WithRequestID(r.Header.Get("X-Request-ID"))
} else {
response = replify.New().
WithStatusCode(200).
WithBody(user).
WithMessage("User retrieved successfully").
WithRequestID(r.Header.Get("X-Request-ID"))
}
respondJSON(w, response)
}
// POST /users
func CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
response := replify.New().
WithStatusCode(400).
WithError(err.Error()).
WithMessage("Invalid request body")
respondJSON(w, response)
return
}
if err := validateUser(user); err != nil {
response := replify.New().
WithStatusCode(422).
WithError(err.Error()).
WithMessage("Validation failed")
respondJSON(w, response)
return
}
createdUser, err := createUser(user)
if err != nil {
response := replify.New().
WithStatusCode(500).
WithErrorAck(err).
WithMessage("Failed to create user")
respondJSON(w, response)
return
}
response := replify.New().
WithStatusCode(201).
WithBody(createdUser).
WithMessage("User created successfully")
respondJSON(w, response)
}
func respondJSON(w http.ResponseWriter, response *replify.R) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode())
w.Write([]byte(response.Json()))
}func ListUsers(w http.ResponseWriter, r *http.Request) {
// Parse query parameters
page := getQueryInt(r, "page", 1)
perPage := getQueryInt(r, "per_page", 10)
search := r.URL.Query().Get("search")
// Fetch users with pagination
users, total, err := db.FindUsers(search, page, perPage)
if err != nil {
response := replify.New().
WithStatusCode(500).
WithErrorAck(err).
WithMessage("Failed to fetch users").
WithDebuggingKV("search", search).
WithDebuggingKV("page", page)
respondJSON(w, response)
return
}
// Calculate pagination metadata
totalPages := (total + perPage - 1) / perPage
isLast := page >= totalPages
pagination := replify.Pages().
WithPage(page).
WithPerPage(perPage).
WithTotalItems(total).
WithTotalPages(totalPages).
WithIsLast(isLast)
response := 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"))
respondJSON(w, response)
}func ProcessOrder(w http.ResponseWriter, r *http.Request) {
order, err := processOrderLogic(r)
response := replify.New()
if err != nil {
response.
WithStatusCode(500).
WithErrorAck(err).
WithMessage("Order processing failed")
// Add debug info in development
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")
}
respondJSON(w, response)
}type R struct {
*wrapper
}The R type 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 JSON string to wrapper |
| 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 |
| 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 |
| 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 if current page is last |
| 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 |
| 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 |
OnKeyDebugging(key string) |
interface{} |
Gets specific debug value |
| Method | Returns | Description |
|---|---|---|
IsSuccess() |
bool |
Checks if status is 2xx |
IsClientError() |
bool |
Checks if status is 4xx |
IsServerError() |
bool |
Checks if status is 5xx |
IsRedirection() |
bool |
Checks if status is 3xx |
IsError() |
bool |
Checks if error exists or status is 4xx/5xx |
IsErrorPresent() |
bool |
Checks if error field exists |
IsBodyPresent() |
bool |
Checks if body exists |
IsPagingPresent() |
bool |
Checks if pagination exists |
IsMetaPresent() |
bool |
Checks if metadata exists |
IsHeaderPresent() |
bool |
Checks if header exists |
IsDebuggingPresent() |
bool |
Checks if debug info exists |
IsDebuggingKeyPresent(key string) |
bool |
Checks if specific debug key exists |
IsLastPage() |
bool |
Checks if current page is last |
IsStatusCodePresent() |
bool |
Checks if valid status code exists |
IsTotalPresent() |
bool |
Checks if total count exists |
| Method | Returns | Description |
|---|---|---|
Json() |
string |
Returns compact JSON string |
JsonPretty() |
string |
Returns pretty-printed JSON |
Respond() |
map[string]interface{} |
Returns map representation |
Reply() |
R |
Returns R wrapper |
| 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 - File upload starts |
| Validation Errors | 400 Bad Request | POST /users - Missing 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 |
| 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 |
| 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 |
| 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 |
| 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 |
-
Always set status codes
response := replify.New(). WithStatusCode(200). WithBody(data)
-
Use request IDs for tracing
response := replify.New(). WithRequestID(r.Header.Get("X-Request-ID")). WithBody(data)
-
Include API version
response := replify.New(). WithApiVersion("v1.0.0"). WithBody(data)
-
Use WithErrorAck for stack traces
response := replify.New(). WithStatusCode(500). WithErrorAck(err)
-
Check response status before processing
if response.IsSuccess() { processData(response.Body()) }
-
Use pagination for list endpoints
pagination := replify.Pages(). WithPage(page). WithPerPage(perPage). WithTotalItems(total)
-
Don't forget to set status codes
// ❌ Bad response := replify.New().WithBody(data) // ✅ Good response := replify.New().WithStatusCode(200).WithBody(data)
-
Don't expose sensitive debug info in production
// ❌ Bad response := replify.New(). WithDebuggingKV("database_password", dbPass) // ✅ Good if os.Getenv("ENV") == "development" { response.WithDebuggingKV("query", sqlQuery) }
-
Don't use generic error messages
// ❌ Bad WithError("Error occurred") // ✅ Good WithError("Failed to create user: email already exists")
-
Don't ignore error checking
// ❌ Bad wrapper, _ := replify.UnwrapJSON(jsonStr) // ✅ Good wrapper, err := replify.UnwrapJSON(jsonStr) if err != nil { log.Printf("Failed to parse JSON: %v", err) }
- 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
- 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 - Direct JSON encoding may be faster
To contribute to this project, follow these steps:
-
Clone the repository
git clone --depth 1 https://github.com/sivaosorg/replify.git
-
Navigate to the project directory
cd replify -
Prepare the project environment
go mod tidy
-
Make your changes
- Follow Go best practices
- Add tests for new features
- Update documentation
-
Run tests
go test ./... -
Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
Part of the replify ecosystem:
- 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
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Built with ❤️ for the Go community.