Frontend and Backend Communication
Frontend and Backend Communication
Overview
Frontends and backends talk through contracts: URLs, methods, status codes, and JSON shapes. Keep those contracts stable, document them lightly, and use the same habits when your Bookstore calls payment gateways or other third-party APIs. This chapter covers JSON interchange, client patterns, and external integration without framework lock-in.
API contract basics
Agree on:
- Resource paths —
/api/books,/api/books/{id} - Methods and status codes — 200/201/4xx/5xx meanings
- JSON field names — snake_case or camelCase; pick one and stick to it
- Error envelope —
{ "error": "...", "code": "..." } - Auth — cookie session vs
Authorization: Bearer ...
Example list response:
[
{
"id": "b1",
"title": "The Go Programming Language",
"author": "Donovan & Kernighan",
"isbn": "978-0134190440",
"price_cents": 4499
}
]Example error:
{
"error": "title is required",
"code": "validation"
}CORS (browser frontends on another origin)
If the UI is http://localhost:5173 and API is http://localhost:8080, browsers enforce CORS.
func withCORS(next http.Handler, allowedOrigin string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}For cookie auth cross-origin, the frontend must use credentials: "include" and the server must not use Allow-Origin: * with credentials.
Simpler path for learning: serve HTML from the same Go process (chapter 274) and skip CORS entirely.
Fetch from a browser (sketch)
const res = await fetch("/api/books", { credentials: "same-origin" });
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || res.statusText);
}
const books = await res.json();Always check res.ok before assuming JSON is a success payload.
Go HTTP client patterns
Reusable client for third parties (payments, shipping, ISBN lookup):
type PaymentGateway struct {
base string
apiKey string
http *http.Client
}
func NewPaymentGateway(base, apiKey string) *PaymentGateway {
return &PaymentGateway{
base: strings.TrimRight(base, "/"),
apiKey: apiKey,
http: &http.Client{
Timeout: 15 * time.Second,
},
}
}
type ChargeRequest struct {
AmountCents int `json:"amount_cents"`
Currency string `json:"currency"`
OrderID string `json:"order_id"`
}
type ChargeResponse struct {
ChargeID string `json:"charge_id"`
Status string `json:"status"`
}
func (g *PaymentGateway) Charge(ctx context.Context, in ChargeRequest) (ChargeResponse, error) {
body, err := json.Marshal(in)
if err != nil {
return ChargeResponse{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, g.base+"/v1/charges", bytes.NewReader(body))
if err != nil {
return ChargeResponse{}, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+g.apiKey)
res, err := g.http.Do(req)
if err != nil {
return ChargeResponse{}, err
}
defer res.Body.Close()
if res.StatusCode >= 300 {
b, _ := io.ReadAll(io.LimitReader(res.Body, 4096))
return ChargeResponse{}, fmt.Errorf("payment gateway %s: %s", res.Status, b)
}
var out ChargeResponse
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
return ChargeResponse{}, err
}
return out, nil
}Client checklist
| Practice | Why |
|---|---|
| Context on every call | Timeouts and cancel |
Explicit http.Client timeout |
No hung requests forever |
| Limit error body reads | Avoid memory blowups |
| Interface for the gateway | Mock in tests (chapter 279) |
| Idempotency keys (when API supports) | Safe retries on POST |
Retries (simple, careful)
Retry idempotent GETs on network blips. Do not blindly retry POSTs that charge cards unless the API is idempotent.
func getWithRetry(ctx context.Context, client *http.Client, url string, attempts int) (*http.Response, error) {
var last error
for i := 0; i < attempts; i++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
res, err := client.Do(req)
if err == nil {
return res, nil
}
last = err
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(time.Duration(i+1) * 100 * time.Millisecond):
}
}
return nil, last
}Content negotiation
Same resource, two representations:
Accept: application/json→ JSON handler
- Browser navigation → HTML handler
Or separate paths (/api/books vs /books)—clearer for many teams.
Rules of thumb
| Do | Don’t |
|---|---|
| Document JSON fields your UI depends on | Rename fields casually |
| Same error shape everywhere | Mix string bodies and JSON errors |
| Interface external APIs | Call payment URLs deep inside handlers with no seam |
| Prefer same-origin for learning apps | Enable wide-open CORS in production |
Try next
- Write a short markdown “API contract” for five Bookstore endpoints.
- Implement a fake
ISBNClientinterface that returns fixed metadata. - From curl, exercise login cookie + authenticated create book as a frontend would.