Frontend and Backend Communication

Updated

September 8, 2026

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:

  1. Resource paths/api/books, /api/books/{id}
  2. Methods and status codes — 200/201/4xx/5xx meanings
  3. JSON field names — snake_case or camelCase; pick one and stick to it
  4. Error envelope{ "error": "...", "code": "..." }
  5. 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"
}

JSON tags and stability

type BookDTO struct {
    ID     string `json:"id"`
    Title  string `json:"title"`
    Author string `json:"author"`
    ISBN   string `json:"isbn"`
    Price  int    `json:"price_cents"`
}
  • Omit sensitive fields with json:"-" (password hashes).
  • Prefer additive changes (new optional fields) over renames.
  • Version only when you must (/api/v2/...)—many Bookstores never need it early.

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

  1. Write a short markdown “API contract” for five Bookstore endpoints.
  2. Implement a fake ISBNClient interface that returns fixed metadata.
  3. From curl, exercise login cookie + authenticated create book as a frontend would.