Day 56 — gRPC or deepen REST (A)

Updated

July 30, 2026

Day 56 — gRPC or deepen REST (A)

Stage VI · ~3h · elective branch
Goal: Choose Track gRPC or Track REST-deep—implement a meaningful slice today; Day 57 continues the same branch on Go 1.26.

Important

Pick one track for Days 56–57. Switching mid-stream wastes the elective. Record the choice in README on day one.

Why this day exists

Not every service needs gRPC. Not every REST API is “done” after CRUD. Experts choose deliberately:

  • Multi-language internal mesh, streaming, strict contracts → gRPC often wins
  • Public HTTP/JSON, browser clients, simple edge caching → REST/JSON often wins

Both are valid. Cargo-culting either is not. Today you implement a real slice of one track so Day 57 can polish (interceptors/streaming lite or gateway/docs polish)—not start from zero.


Theory 1 — Track selection guide

Signal Lean gRPC Lean REST-deep
Browser first-class
Mobile/web public API
Internal service-to-service
Bi-di streaming needs SSE/WS (Day 59)
Team already JSON/HTTP
Strong schema + codegen culture OpenAPI
Load balancers / HTTP-only edge ✓ (or grpc-gateway)
Strict binary contracts + versions possible with OpenAPI

Decision record (required)

## Elective track (Days 56–57)

Choice: gRPC | REST-deep
Why (3 bullets):
1.
2.
3.
Out of scope this elective:

Paste into README or ELECTIVE-TRACK.md.


Track G — gRPC unary intro

Theory G1 — Stack overview

.proto
  → protoc
  → protoc-gen-go       (messages)
  → protoc-gen-go-grpc  (service interfaces)
  → generated stubs in module
server implements FooServer interface
client dials (transport creds) and calls RPC methods
Piece Role
Transport HTTP/2
Payload Protobuf (binary)
Stubs Typed Go interfaces + structs
Errors google.golang.org/grpc/status + codes

Toolchain (record versions)

protoc --version
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
which protoc-gen-go protoc-gen-go-grpc

Keep plugin versions compatible with your module’s google.golang.org/grpc and protobuf requires. Pin in notes if CI must reproduce.


Theory G2 — Minimal proto

syntax = "proto3";
package items.v1;
option go_package = "example.com/day56/gen/itemsv1;itemsv1";

service ItemService {
  rpc GetItem(GetItemRequest) returns (GetItemResponse);
  rpc CreateItem(CreateItemRequest) returns (CreateItemResponse);
}

message GetItemRequest { string id = 1; }
message GetItemResponse {
  string id = 1;
  string name = 2;
}
message CreateItemRequest { string name = 1; }
message CreateItemResponse {
  string id = 1;
  string name = 2;
}

Generate (adjust paths to your layout):

mkdir -p gen/itemsv1
protoc \
  --go_out=gen --go_opt=paths=source_relative \
  --go-grpc_out=gen --go-grpc_opt=paths=source_relative \
  api/proto/items/v1/items.proto

Document whether generated code is committed or produced in CI—pick one policy and stick to it.


Theory G3 — Server implementation

Embed Unimplemented*Server for forward compatibility:

type Server struct {
    itemsv1.UnimplementedItemServiceServer
    repo ItemRepo // your Stage VI store interface
}

func (s *Server) GetItem(ctx context.Context, req *itemsv1.GetItemRequest) (*itemsv1.GetItemResponse, error) {
    if req.GetId() == "" {
        return nil, status.Error(codes.InvalidArgument, "id required")
    }
    it, err := s.repo.Get(ctx, req.GetId())
    if errors.Is(err, store.ErrNotFound) {
        return nil, status.Error(codes.NotFound, "item not found")
    }
    if err != nil {
        return nil, status.Error(codes.Internal, "internal")
    }
    return &itemsv1.GetItemResponse{Id: it.ID, Name: it.Name}, nil
}

func (s *Server) CreateItem(ctx context.Context, req *itemsv1.CreateItemRequest) (*itemsv1.CreateItemResponse, error) {
    name := strings.TrimSpace(req.GetName())
    if name == "" {
        return nil, status.Error(codes.InvalidArgument, "name required")
    }
    it, err := s.repo.Create(ctx, name)
    if err != nil {
        return nil, status.Error(codes.Internal, "internal")
    }
    return &itemsv1.CreateItemResponse{Id: it.ID, Name: it.Name}, nil
}

Listen and serve

lis, err := net.Listen("tcp", ":50051")
if err != nil {
    log.Fatal(err)
}
gs := grpc.NewServer()
itemsv1.RegisterItemServiceServer(gs, &Server{repo: repo})
log.Info("grpc listen", "addr", lis.Addr().String())
if err := gs.Serve(lis); err != nil {
    log.Fatal(err)
}

Graceful stop (wire to signal later):

go func() {
    <-ctx.Done()
    gs.GracefulStop()
}()

Theory G4 — Status codes map

Situation codes.*
Bad client input InvalidArgument
Missing entity NotFound
Duplicate AlreadyExists
No/invalid auth Unauthenticated
Authenticated but forbidden PermissionDenied
Unexpected server bug Internal
Deadline exceeded DeadlineExceeded

Avoid overusing Unknown. Never panic for “not found.”


Theory G5 — Client smoke + tests

conn, err := grpc.NewClient("127.0.0.1:50051",
    grpc.WithTransportCredentials(insecure.NewCredentials()))
client := itemsv1.NewItemServiceClient(conn)
_, err = client.GetItem(ctx, &itemsv1.GetItemRequest{Id: "missing"})
st, _ := status.FromError(err)
// expect codes.NotFound

Manual: grpcurl -plaintext localhost:50051 list and invoke GetItem/CreateItem. Prefer bufconn in-process for unit tests; network smoke is fine for the lab.


Lab G — multi-step

  1. Install protoc + plugins; record versions in README.
  2. Author proto; generate stubs; document commit-vs-CI policy.
  3. Implement Get + Create unary against your existing repo/store.
  4. Map validation → InvalidArgument, missing → NotFound.
  5. Smoke with grpcurl or Go client test (go test ./... -count=1).
  6. Write run instructions (port, generate command, insecure local-only note).
  7. Plan Day 57: interceptors (logging/auth) and/or server streaming lite.

Track R — deepen REST

Theory R1 — Conditional requests & ETags

ETags let clients and caches avoid shipping unchanged bodies:

func etagFor(body []byte) string {
    sum := sha256.Sum256(body)
    return fmt.Sprintf(`"%x"`, sum[:8])
}

func writeItemJSON(w http.ResponseWriter, r *http.Request, it Item) {
    body, err := json.Marshal(it)
    if err != nil {
        httpapi.WriteInternal(w)
        return
    }
    tag := etagFor(body)
    w.Header().Set("ETag", tag)
    w.Header().Set("Content-Type", "application/json")
    if match := r.Header.Get("If-None-Match"); match == tag {
        w.WriteHeader(http.StatusNotModified)
        return
    }
    w.WriteHeader(http.StatusOK)
    _, _ = w.Write(body)
}

Useful for GET caching (ties Day 58). Weak ETags (W/"...") exist; strong is fine for lab JSON.


Theory R2 — Bulk / batch endpoints

POST /v1/items:batchGet
{"ids":["1","2","3"]}

Rules:

  • Hard max (e.g. 100)
  • Partial success policy documented (all-or-nothing vs per-id errors)
  • Auth still required if single-get is protected
const maxBatch = 100

type batchGetReq struct {
    IDs []string `json:"ids"`
}

func (h *Handler) BatchGet(w http.ResponseWriter, r *http.Request) {
    r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
    var req batchGetReq
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        h.writeErr(w, errInvalid)
        return
    }
    if len(req.IDs) == 0 || len(req.IDs) > maxBatch {
        h.writeErr(w, errInvalid)
        return
    }
    // fetch; return {"items":[...]} 
}

Theory R3 — Partial representation

Options:

Approach Example Tradeoff
Query fields ?fields=id,name Flexible; parser cost
Separate DTOs list DTO vs detail DTO Clear; more types
Sparse fieldsets later JSON:API-style Heavy for this volume

For the lab, list DTO vs detail DTO is enough:

type ItemListDTO struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}
type ItemDetailDTO struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    CreatedAt time.Time `json:"created_at"`
    // owner, etc.
}

Theory R4 — OpenAPI sketch + Idempotency-Key

Author a honest openapi.yaml for core v1 routes (paths, schemas, bearerAuth). Generating clients is optional; authoring discipline is the skill—do not document routes that 404.

Clients retry POSTs; without idempotency keys, retries create duplicates:

key := r.Header.Get("Idempotency-Key")
if key != "" {
    if resp, ok := h.idem.Get(key); ok {
        writeCached(w, resp)
        return
    }
}
// create ... then h.idem.Put(key, status, body)  // lab: in-memory + TTL note

Lab R — multi-step

  1. Add ETag + If-None-Match on GET /v1/items/{id}; httptest 304.
  2. Add batch get with hard limit; test over-limit → 400.
  3. Draft openapi.yaml for core routes (auth scheme included).
  4. Idempotency-Key on create (in-memory map OK); test replay returns same body/id.
  5. Plan Day 57: middleware polish, docs publish, tiny client SDK snippet.

Shared requirements (both tracks)

Requirement gRPC REST-deep
README choice declared
Reuses store/repo from Stage VI
Tests client RPC or bufconn httptest
Error mapping discipline codes.* Day 55 envelope
Body/input limits message size awareness MaxBytesReader
No secret commits
Go 1.26 module

Shared lab wrap-up

  1. Update architecture paragraph: “public surface is gRPC on :50051” or “REST deepened with ETag/batch/idempotency.”
  2. Note what Day 62 gate will require if you keep this elective.
  3. Do not half-merge the other track into the binary.

Hour-by-hour suggestion

Time Focus
0:00–0:20 Choose track; write decision record
0:20–1:40 Core implementation (proto+server or ETag+batch)
1:40–2:20 Error mapping + tests
2:20–2:45 Docs (generate policy / openapi)
2:45–3:00 Day 57 plan bullets

Common gotchas

Track Gotcha Fix
G Checking in generated code inconsistently Document generate or commit deliberately
G Using panic for not found status.Error
G Insecure dial in prod notes TLS Day 60 awareness; local-only disclaimer
G Forgetting Unimplemented*Server embed Embed for forward compat
R Unbounded batch Max N
R Weak ETag over mutable unprotected data Auth + ownership
R OpenAPI drift Only document implemented routes
R Idempotency store unbounded growth TTL + max entries in lab notes
Both New store instead of reusing Wire existing repo

Checkpoint

  • Track chosen and documented
  • End-to-end happy path works
  • Error mapping tested (at least one non-happy path)
  • Reuses existing persistence/auth where applicable
  • Plan written for Day 57 continuation
  • README run instructions cold-tested

Commit

git add .
git commit -m "day56: elective track A (grpc or rest-deep)"

Write three personal gotchas before continuing.


Tomorrow

Day 57 — continue branch B: gRPC interceptors/streaming lite or REST gateway polish (middleware, docs, client SDK snippet). Same track—do not switch.