Day 57 — gRPC or deepen REST (B)
Day 57 — gRPC or deepen REST (B)
Stage VI · ~3h · elective continuation
Goal: Continue the same track as Day 56—add production-shaped edges (interceptors/streaming lite or REST polish with docs, hardening, and a tiny client).
Why this day exists
Day 56 proved the stack boots. Day 57 adds the pieces that separate demos from services:
- Cross-cutting concerns (auth, logging, deadlines)
- Streaming or polish that matches your track
- Client experience (grpcurl/Go client or OpenAPI-driven curl SDK)
Track G — interceptors & streaming lite
Theory G1 — Unary interceptor
func UnaryServerLogging(log *slog.Logger) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Info("rpc",
"method", info.FullMethod,
"dur_ms", time.Since(start).Milliseconds(),
"err", err,
)
return resp, err
}
}
gs := grpc.NewServer(grpc.UnaryInterceptor(UnaryServerLogging(log)))Chain with grpc.ChainUnaryInterceptor(auth, logging, recover).
Theory G2 — Auth interceptor
Read metadata for authorization bearer token; validate; put user in context. Reject with codes.Unauthenticated.
md, ok := metadata.FromIncomingContext(ctx)
// md.Get("authorization")Theory G3 — Server streaming (lite)
rpc ListItems(ListItemsRequest) returns (stream Item);func (s *Server) ListItems(req *itemsv1.ListItemsRequest, stream itemsv1.ItemService_ListItemsServer) error {
items, err := s.repo.List(stream.Context())
if err != nil {
return status.Error(codes.Internal, "internal")
}
for _, it := range items {
if err := stream.Send(&itemsv1.Item{Id: it.ID, Name: it.Name}); err != nil {
return err
}
}
return nil
}Client-streaming / bidi: awareness only unless time allows.
Theory G4 — Deadlines
Clients should set:
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()Servers honor ctx.Done() in handlers and DB calls.
Worked example G — recover interceptor
func UnaryRecover(log *slog.Logger) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
defer func() {
if rec := recover(); rec != nil {
log.Error("panic", "method", info.FullMethod, "recover", rec)
err = status.Error(codes.Internal, "internal")
}
}()
return handler(ctx, req)
}
}Wire order (outermost first in chain helpers—check docs for ChainUnaryInterceptor application order):
recover → logging → auth → handler
Lab G
mkdir -p ~/lab/90daysofx/01-go/day57
# continue day56 module; same go.mod- Logging + auth + recover unary interceptors
- One streaming RPC or richer
status.WithDetailsvalidation errors
- Client test with timeout asserting cancellation
- README: interceptors order diagram (ASCII)
grpcurl -H 'authorization: Bearer …' list/callexamples
- Prove unauthenticated call returns
Unauthenticated
# illustrative
grpcurl -plaintext -d '{"id":"1"}' 127.0.0.1:50051 items.v1.ItemService/GetItem
# expect auth error after interceptor landsTrack R — REST gateway polish
Theory R1 — Middleware completeness
Ensure chain: Recover → RequestID → AccessLog → Auth (where needed) → handlers.
Add maxBytes middleware:
func MaxBytes(n int64) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Body != nil {
r.Body = http.MaxBytesReader(w, r.Body, n)
}
next.ServeHTTP(w, r)
})
}
}Also reject non-JSON Content-Type on write methods:
func RequireJSON(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost, http.MethodPut, http.MethodPatch:
ct := r.Header.Get("Content-Type")
if !strings.HasPrefix(ct, "application/json") {
writeErr(w, http.StatusUnsupportedMediaType, "unsupported_media_type", "Content-Type must be application/json")
return
}
}
next.ServeHTTP(w, r)
})
}Theory R2 — Consistent CORS (if browser)
Only if you have a browser client—default deny. For lab APIs, document “no CORS” or tight allowlist. Never Access-Control-Allow-Origin: * with credentialed cookies.
Theory R3 — Client package
Ship package apiclient wrapping Day 46 patterns:
type Client struct {
base string
http *http.Client
token string
}
func New(base string, opts ...Option) *Client { /* timeouts from Day 46 */ }
func (c *Client) WithToken(tok string) *Client {
cp := *c
cp.token = tok
return &cp
}
func (c *Client) CreateItem(ctx context.Context, name string) (Item, error) {
// POST /v1/items with Authorization if token set
}Tests against httptest.NewServer using your real api.Handler().
Theory R4 — Docs polish
- Expand OpenAPI with
securitySchemes(http bearer or cookie)
scripts/smoke.shwith auth login → create → get
- Example error payloads from Day 55 copied into docs
Theory R5 — Observability hooks (light)
Log request_id on every error path; ensure 5xx always slog.Error with err. Add a /v1/version endpoint returning git SHA or build time injected via -ldflags (optional stretch).
Worked example R — smoke script
#!/usr/bin/env bash
set -euo pipefail
base="${BASE_URL:-http://127.0.0.1:8080}"
tok=$(curl -sf -X POST "$base/v1/login" \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"secret"}' | jq -r .access_token)
curl -sf -X POST "$base/v1/items" \
-H "Authorization: Bearer $tok" \
-H 'Content-Type: application/json' \
-d '{"name":"widget"}' | jq .Lab R
- Hardening middleware (max bytes, strict Content-Type)
- Typed Go API client package + tests
- OpenAPI security + examples updated
- Smoke script with JWT/session
- Optional: ETag on list if not done Day 56
go test ./... -raceon client + api packages
Decision checkpoint (both)
Write 10–15 lines in README:
- Would you keep this track for the capstone? Why?
- What would you drop if time-constrained?
- What dependency (protoc plugins / OpenAPI tools) hurt the most?
Common gotchas
| Gotcha | Fix |
|---|---|
| Interceptor order wrong | Auth before handler; recover outermost |
| Stream without ctx checks | Select on ctx.Done for long lists |
| Client without timeout | Always |
| OpenAPI not matching routes | Diff manually once |
CORS * + credentials |
Never |
| Leaving Day 56 half-broken after “polish” | Keep green tests before new features |
| Committing huge generated trees without docs | Document make generate |
Checkpoint
- Same track as Day 56 continued
- Cross-cutting auth/logging improved
- Stream or client package shipped
- Hardening (timeouts / max bytes / recover) present
- Tests green including at least one auth failure case
- Capstone lean note written
Commit
git add .
git commit -m "day57: elective track B continuation"Write three personal gotchas before continuing.
Tomorrow
Day 58 — caching: cache-aside pattern, in-memory (and optional Redis) behind an interface, invalidation discipline.