diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..723ef36
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.idea
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/httpsuite.iml b/.idea/httpsuite.iml
deleted file mode 100644
index 5e764c4..0000000
--- a/.idea/httpsuite.iml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 4868574..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 55f2d5d..e38f2be 100644
--- a/README.md
+++ b/README.md
@@ -66,12 +66,13 @@ func main() {
}
// Step 2: Send a success response
- httpsuite.SendResponse(w, "Request received successfully", http.StatusOK, &req)
+ httpsuite.SendResponse[SampleRequest](w, http.StatusOK, *req, nil, nil)
})
log.Println("Starting server on :8080")
http.ListenAndServe(":8080", r)
}
+
```
Check out the [example folder for a complete project](./examples) demonstrating how to integrate **httpsuite** into
diff --git a/examples/main.go b/examples/main.go
index ebf9591..22b8b94 100644
--- a/examples/main.go
+++ b/examples/main.go
@@ -35,7 +35,7 @@ func main() {
}
// Step 2: Send a success response
- httpsuite.SendResponse(w, "Request received successfully", http.StatusOK, &req)
+ httpsuite.SendResponse[SampleRequest](w, http.StatusOK, *req, nil, nil)
})
log.Println("Starting server on :8080")
diff --git a/request.go b/request.go
index 36af1b2..72c3e2b 100644
--- a/request.go
+++ b/request.go
@@ -30,7 +30,8 @@ func ParseRequest[T RequestParamSetter](w http.ResponseWriter, r *http.Request,
if r.Body != http.NoBody {
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
- SendResponse[any](w, "Invalid JSON format", http.StatusBadRequest, nil)
+ SendResponse[any](w, http.StatusBadRequest, nil,
+ []Error{{Code: http.StatusBadRequest, Message: "Invalid JSON format"}}, nil)
return empty, err
}
}
@@ -44,19 +45,22 @@ func ParseRequest[T RequestParamSetter](w http.ResponseWriter, r *http.Request,
for _, key := range pathParams {
value := chi.URLParam(r, key)
if value == "" {
- SendResponse[any](w, "Parameter "+key+" not found in request", http.StatusBadRequest, nil)
+ SendResponse[any](w, http.StatusBadRequest, nil,
+ []Error{{Code: http.StatusBadRequest, Message: "Parameter " + key + " not found in request"}}, nil)
return empty, errors.New("missing parameter: " + key)
}
if err := request.SetParam(key, value); err != nil {
- SendResponse[any](w, "Failed to set field "+key, http.StatusInternalServerError, nil)
+ SendResponse[any](w, http.StatusInternalServerError, nil,
+ []Error{{Code: http.StatusInternalServerError, Message: "Failed to set field " + key, Details: err.Error()}}, nil)
return empty, err
}
}
// Validate the combined request struct
if validationErr := IsRequestValid(request); validationErr != nil {
- SendResponse[ValidationErrors](w, "Validation error", http.StatusBadRequest, validationErr)
+ SendResponse[any](w, http.StatusBadRequest, nil,
+ []Error{{Code: http.StatusBadRequest, Message: "Validation error", Details: validationErr}}, nil)
return empty, errors.New("validation error")
}
diff --git a/response.go b/response.go
index adb12fd..67931bb 100644
--- a/response.go
+++ b/response.go
@@ -1,53 +1,80 @@
package httpsuite
import (
+ "bytes"
"encoding/json"
"log"
"net/http"
)
// Response represents the structure of an HTTP response, including a status code, message, and optional body.
+// T represents the type of the `Data` field, allowing this structure to be used flexibly across different endpoints.
type Response[T any] struct {
- Code int `json:"code"`
- Message string `json:"message"`
- Body T `json:"body,omitempty"`
+ Data T `json:"data,omitempty"`
+ Errors []Error `json:"errors,omitempty"`
+ Meta *Meta `json:"meta,omitempty"`
}
-// Marshal serializes the Response struct into a JSON byte slice.
-// It logs an error if marshalling fails.
-func (r *Response[T]) Marshal() []byte {
- jsonResponse, err := json.Marshal(r)
- if err != nil {
- log.Printf("failed to marshal response: %v", err)
- }
+// Error represents an error in the aPI response, with a structured format to describe issues in a consistent manner.
+type Error struct {
+ // Code unique error code or HTTP status code for categorizing the error
+ Code int `json:"code"`
+ // Message user-friendly message describing the error.
+ Message string `json:"message"`
+ // Details additional details about the error, often used for validation errors.
+ Details interface{} `json:"details,omitempty"`
+}
- return jsonResponse
+// Meta provides additional information about the response, such as pagination details.
+// This is particularly useful for endpoints returning lists of data.
+type Meta struct {
+ // Page the current page number
+ Page int `json:"page,omitempty"`
+ // PageSize the number of items per page
+ PageSize int `json:"page_size,omitempty"`
+ // TotalPages the total number of pages available.
+ TotalPages int `json:"total_pages,omitempty"`
+ // TotalItems the total number of items across all pages.
+ TotalItems int `json:"total_items,omitempty"`
}
-// SendResponse creates a Response struct, serializes it to JSON, and writes it to the provided http.ResponseWriter.
-// If the body parameter is non-nil, it will be included in the response body.
-func SendResponse[T any](w http.ResponseWriter, message string, code int, body *T) {
+// SendResponse sends a JSON response to the client, using a unified structure for both success and error responses.
+// T represents the type of the `data` payload. This function automatically adapts the response structure
+// based on whether `data` or `errors` is provided, promoting a consistent API format.
+//
+// Parameters:
+// - w: The http.ResponseWriter to send the response.
+// - code: HTTP status code to indicate success or failure.
+// - data: The main payload of the response. Use `nil` for error responses.
+// - errs: A slice of Error structs to describe issues. Use `nil` for successful responses.
+// - meta: Optional metadata, such as pagination information. Use `nil` if not needed.
+func SendResponse[T any](w http.ResponseWriter, code int, data T, errs []Error, meta *Meta) {
+ w.Header().Set("Content-Type", "application/json")
+
response := &Response[T]{
- Code: code,
- Message: message,
- }
- if body != nil {
- response.Body = *body
+ Data: data,
+ Errors: errs,
+ Meta: meta,
}
- writeResponse[T](w, response)
-}
+ // Set the status code after encoding to ensure no issues with writing the response body
+ w.WriteHeader(code)
+
+ // Attempt to encode the response as JSON
+ var buffer bytes.Buffer
+ if err := json.NewEncoder(&buffer).Encode(response); err != nil {
+ log.Printf("Error writing response: %v", err)
-// writeResponse serializes a Response and writes it to the http.ResponseWriter with appropriate headers.
-// If an error occurs during the write, it logs the error and sends a 500 Internal Server Error response.
-func writeResponse[T any](w http.ResponseWriter, r *Response[T]) {
- jsonResponse := r.Marshal()
+ errResponse := `{"errors":[{"code":500,"message":"Internal Server Error"}]}`
+ http.Error(w, errResponse, http.StatusInternalServerError)
+ return
+ }
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(r.Code)
+ // Set the status code after success encoding
+ w.WriteHeader(code)
- if _, err := w.Write(jsonResponse); err != nil {
+ // Write the encoded response to the ResponseWriter
+ if _, err := w.Write(buffer.Bytes()); err != nil {
log.Printf("Error writing response: %v", err)
- http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
diff --git a/response_test.go b/response_test.go
index 2df7b4a..32c764a 100644
--- a/response_test.go
+++ b/response_test.go
@@ -12,115 +12,63 @@ type TestResponse struct {
Key string `json:"key"`
}
-func TestResponse_Marshal(t *testing.T) {
+func Test_SendResponse(t *testing.T) {
tests := []struct {
- name string
- response Response[any]
- expected string
+ name string
+ code int
+ data any
+ errs []Error
+ meta *Meta
+ expectedCode int
+ expectedJSON string
}{
{
- name: "Basic Response",
- response: Response[any]{Code: 200, Message: "OK"},
- expected: `{"code":200,"message":"OK"}`,
+ name: "200 OK with TestResponse body",
+ code: http.StatusOK,
+ data: &TestResponse{Key: "value"},
+ errs: nil,
+ expectedCode: http.StatusOK,
+ expectedJSON: `{"data":{"key":"value"}}`,
},
{
- name: "Response with Body",
- response: Response[any]{Code: 201, Message: "Created", Body: map[string]string{"id": "123"}},
- expected: `{"code":201,"message":"Created","body":{"id":"123"}}`,
+ name: "404 Not Found without body",
+ code: http.StatusNotFound,
+ data: nil,
+ errs: []Error{{Code: http.StatusNotFound, Message: "Not Found"}},
+ expectedCode: http.StatusNotFound,
+ expectedJSON: `{"errors":[{"code":404,"message":"Not Found"}]}`,
},
{
- name: "Response with Empty Body",
- response: Response[any]{Code: 204, Message: "No Content", Body: nil},
- expected: `{"code":204,"message":"No Content"}`,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- jsonResponse := tt.response.Marshal()
- assert.JSONEq(t, tt.expected, string(jsonResponse))
- })
- }
-}
-
-func Test_SendResponse(t *testing.T) {
- tests := []struct {
- name string
- message string
- code int
- body any
- expectedCode int
- expectedBody string
- expectedHeader string
- }{
- {
- name: "200 OK with TestResponse body",
- message: "Success",
- code: http.StatusOK,
- body: &TestResponse{Key: "value"},
- expectedCode: http.StatusOK,
- expectedBody: `{"code":200,"message":"Success","body":{"key":"value"}}`,
- expectedHeader: "application/json",
+ name: "200 OK with pagination metadata",
+ code: http.StatusOK,
+ data: &TestResponse{Key: "value"},
+ meta: &Meta{TotalPages: 100, Page: 1, PageSize: 10},
+ expectedCode: http.StatusOK,
+ expectedJSON: `{"data":{"key":"value"},"meta":{"total_pages":100,"page":1,"page_size":10}}`,
},
{
- name: "404 Not Found without body",
- message: "Not Found",
- code: http.StatusNotFound,
- body: nil,
- expectedCode: http.StatusNotFound,
- expectedBody: `{"code":404,"message":"Not Found"}`,
- expectedHeader: "application/json",
+ name: "400 Bad Request with multiple errors",
+ code: http.StatusBadRequest,
+ errs: []Error{{Code: 400, Message: "Invalid email"}, {Code: 400, Message: "Invalid password"}},
+ expectedCode: http.StatusBadRequest,
+ expectedJSON: `{"errors":[{"code":400,"message":"Invalid email"},{"code":400,"message":"Invalid password"}]}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- recorder := httptest.NewRecorder()
+ w := httptest.NewRecorder()
- switch body := tt.body.(type) {
- case *TestResponse:
- SendResponse[TestResponse](recorder, tt.message, tt.code, body)
+ switch data := tt.data.(type) {
+ case TestResponse:
+ SendResponse[TestResponse](w, tt.code, data, tt.errs, tt.meta)
default:
- SendResponse(recorder, tt.message, tt.code, &tt.body)
+ SendResponse[any](w, tt.code, tt.data, tt.errs, tt.meta)
}
- assert.Equal(t, tt.expectedCode, recorder.Code)
- assert.Equal(t, tt.expectedHeader, recorder.Header().Get("Content-Type"))
- assert.JSONEq(t, tt.expectedBody, recorder.Body.String())
- })
- }
-}
-
-func TestWriteResponse(t *testing.T) {
- tests := []struct {
- name string
- response Response[any]
- expectedCode int
- expectedBody string
- }{
- {
- name: "200 OK with Body",
- response: Response[any]{Code: 200, Message: "OK", Body: map[string]string{"id": "123"}},
- expectedCode: 200,
- expectedBody: `{"code":200,"message":"OK","body":{"id":"123"}}`,
- },
- {
- name: "500 Internal Server Error without Body",
- response: Response[any]{Code: 500, Message: "Internal Server Error"},
- expectedCode: 500,
- expectedBody: `{"code":500,"message":"Internal Server Error"}`,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- recorder := httptest.NewRecorder()
-
- writeResponse(recorder, &tt.response)
-
- assert.Equal(t, tt.expectedCode, recorder.Code)
- assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
- assert.JSONEq(t, tt.expectedBody, recorder.Body.String())
+ assert.Equal(t, tt.expectedCode, w.Code)
+ assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
+ assert.JSONEq(t, tt.expectedJSON, w.Body.String())
})
}
}