Blog Logo

17-Jun-2025 ~ 3 min read

Chaining POST Requests in Go with JSON - part 2


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:

  1. Accept a JSON POST request.

  2. Optionally extract and transform fields.

  3. Send a chained POST request to a target backend URL.

  4. Return the backend’s response as-is or with optional wrapping.

Table of Contents

🏗️ 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.