Chaining POST Requests in Go with JSON: Part 2
In the previous part, we set up a basic handler to receive a POST request, extract a foo_id
, and make a POST request to another service. In this part, we will extend that example and build a general purpose POST chaining handler that can:
-
Accept a JSON POST request.
-
Optionally extract and transform fields.
-
Send a chained POST request to a target backend URL.
-
Return the backend’s response as-is or with optional wrapping.
Table of Contents
- 🏗️ High-Level Architecture
- ✅ 1. ChainPostRequest - the generic chaining function
- ✅ 2. Using the Handler in Your Main App
- ✅ 3. Error Helper
- 💡 What’s Good About This Design?
🏗️ High-Level Architecture
We’ll create:
-
A configurable handler for chaining POST requests.
-
An optional transformation function.
-
A reusable core function (ChainPostRequest) that takes care of logic.
✅ 1. ChainPostRequest - the generic chaining function
package postchain
import (
"bytes"
"encoding/json"
"io"
"net/http"
)
type TransformFunc func(map[string]interface{}) (map[string]interface{}, error)
type ChainOptions struct {
TargetURL string
Transform TransformFunc // optional
ForwardAll bool // if true, forwards entire input as-is
}
func ChainPostRequest(w http.ResponseWriter, r *http.Request, opts ChainOptions) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST is allowed", http.StatusMethodNotAllowed)
return
}
var input map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "Invalid JSON body", http.StatusBadRequest)
return
}
var payload map[string]interface{}
var err error
switch {
case opts.ForwardAll:
payload = input
case opts.Transform != nil:
payload, err = opts.Transform(input)
if err != nil {
http.Error(w, "Transformation failed: "+err.Error(), http.StatusBadRequest)
return
}
default:
payload = map[string]interface{}{}
}
postBody, err := json.Marshal(payload)
if err != nil {
http.Error(w, "Failed to marshal chained request", http.StatusInternalServerError)
return
}
resp, err := http.Post(opts.TargetURL, "application/json", bytes.NewBuffer(postBody))
if err != nil {
http.Error(w, "Failed to contact backend: "+err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
✅ 2. Using the Handler in Your Main App
package main
import (
"example.com/postchain"
"net/http"
)
func main() {
http.HandleFunc("/api/foo-status", func(w http.ResponseWriter, r *http.Request) {
postchain.ChainPostRequest(w, r, postchain.ChainOptions{
TargetURL: "https://backend.example.com/render",
Transform: func(input map[string]interface{}) (map[string]interface{}, error) {
// For example: only forward `foo_id` as `id`
id, ok := input["foo_id"].(string)
if !ok {
return nil, ErrMissingField("foo_id")
}
return map[string]interface{}{
"id": id,
"url": "https://some-actual-resource.com",
}, nil
},
})
})
http.ListenAndServe(":8080", nil)
}
✅ 3. Error Helper
package postchain
import "fmt"
func ErrMissingField(field string) error {
return fmt.Errorf("missing or invalid field: %s", field)
}
💡 What’s Good About This Design?
-
✅ Pluggable: You can plug in a transformation function or just forward all data.
-
✅ Reusable: Same core function for many endpoints.
-
✅ Safe: Handles error conditions and preserves status codes.
-
✅ Extendable: You can easily add headers, auth, or logging wrappers.