Claude Code for Go Microservices: HTTP Handlers, gRPC, and Concurrency Patterns — Claude Skills 360 Blog
Blog / Backend / Claude Code for Go Microservices: HTTP Handlers, gRPC, and Concurrency Patterns
Backend

Claude Code for Go Microservices: HTTP Handlers, gRPC, and Concurrency Patterns

Published: October 11, 2026
Read time: 9 min read
By: Claude Skills 360

Go’s combination of static types, goroutines, and explicit error handling makes it well-suited for microservices. The idioms matter: context propagation for cancellation, channels for coordination, and errors as values rather than exceptions. Claude Code writes idiomatic Go with proper HTTP handler structure, gRPC implementations, goroutine lifecycle management, and structured logging with the standard slog package.

CLAUDE.md for Go Projects

## Go Stack
- Go 1.22+
- HTTP: net/http with chi router for complex routing
- gRPC: google.golang.org/grpc + google.golang.org/protobuf
- Database: pgx/v5 directly (not GORM)
- Logging: log/slog (standard library, structured JSON)
- Config: environment variables via os.Getenv, validated at startup
- Testing: standard testing package + testify/assert
- Errors: return errors, wrap with fmt.Errorf("context: %w", err)
- Context: always first parameter, always propagate, never store in structs
- No panic in library code; panic only in main() for unrecoverable startup failures

HTTP Server with Graceful Shutdown

// cmd/api/main.go
package main

import (
    "context"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))
    slog.SetDefault(logger)

    db, err := connectDB(os.Getenv("DATABASE_URL"))
    if err != nil {
        slog.Error("failed to connect to database", "error", err)
        os.Exit(1)
    }
    defer db.Close()

    handlers := &Handlers{db: db, logger: logger}
    router := buildRouter(handlers, logger)

    server := &http.Server{
        Addr:         ":" + getEnvOrDefault("PORT", "8080"),
        Handler:      router,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)

    go func() {
        slog.Info("server starting", "addr", server.Addr)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            slog.Error("server failed", "error", err)
            os.Exit(1)
        }
    }()

    <-quit
    slog.Info("shutting down server")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        slog.Error("server shutdown failed", "error", err)
    }
    slog.Info("server stopped")
}

func buildRouter(h *Handlers, logger *slog.Logger) *chi.Mux {
    r := chi.NewRouter()
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    r.Use(structuredLogger(logger))
    r.Use(middleware.Recoverer)
    r.Use(middleware.Timeout(15 * time.Second))

    r.Get("/healthz", h.healthCheck)
    r.Get("/readyz", h.readinessCheck)

    r.Route("/api/v1", func(r chi.Router) {
        r.Use(authMiddleware)
        r.Route("/orders", func(r chi.Router) {
            r.Get("/", h.listOrders)
            r.Post("/", h.createOrder)
            r.Get("/{id}", h.getOrder)
        })
    })

    return r
}

HTTP Handlers

// internal/handlers/orders.go
type Handlers struct {
    db     *pgxpool.Pool
    logger *slog.Logger
}

func (h *Handlers) createOrder(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    var req CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid request body")
        return
    }

    if err := req.Validate(); err != nil {
        writeError(w, http.StatusUnprocessableEntity, err.Error())
        return
    }

    order, err := h.createOrderInDB(ctx, req)
    if err != nil {
        h.logger.ErrorContext(ctx, "failed to create order",
            "error", err,
            "customer_id", req.CustomerID,
        )
        writeError(w, http.StatusInternalServerError, "failed to create order")
        return
    }

    writeJSON(w, http.StatusCreated, order)
}

func (h *Handlers) createOrderInDB(ctx context.Context, req CreateOrderRequest) (*Order, error) {
    tx, err := h.db.Begin(ctx)
    if err != nil {
        return nil, fmt.Errorf("begin transaction: %w", err)
    }
    defer tx.Rollback(ctx)

    var orderID string
    err = tx.QueryRow(ctx,
        `INSERT INTO orders (customer_id, total_cents, status)
         VALUES ($1, $2, 'PENDING') RETURNING id`,
        req.CustomerID, req.TotalCents,
    ).Scan(&orderID)
    if err != nil {
        return nil, fmt.Errorf("insert order: %w", err)
    }

    if err := tx.Commit(ctx); err != nil {
        return nil, fmt.Errorf("commit: %w", err)
    }

    return &Order{ID: orderID, Status: "PENDING"}, nil
}

func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

Goroutine Patterns

// Worker pool: bounded concurrency
func processOrderBatch(ctx context.Context, orders []Order, workerCount int) error {
    work := make(chan Order, len(orders))
    errs := make(chan error, len(orders))

    var wg sync.WaitGroup
    for range workerCount {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for order := range work {
                if err := processOrder(ctx, order); err != nil {
                    errs <- fmt.Errorf("order %s: %w", order.ID, err)
                }
            }
        }()
    }

    for _, order := range orders {
        work <- order
    }
    close(work)

    wg.Wait()
    close(errs)

    var allErrors []error
    for err := range errs {
        allErrors = append(allErrors, err)
    }
    return errors.Join(allErrors...)
}

// Context timeout on external call
func callExternalAPI(ctx context.Context, url string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, fmt.Errorf("create request: %w", err)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return nil, fmt.Errorf("request timed out after 5s")
        }
        return nil, fmt.Errorf("request failed: %w", err)
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

// Fan-out: launch N goroutines, collect first error
func enrichOrders(ctx context.Context, orders []*Order) error {
    g, ctx := errgroup.WithContext(ctx)

    for _, order := range orders {
        order := order  // Capture loop variable (Go < 1.22)
        g.Go(func() error {
            customer, err := fetchCustomer(ctx, order.CustomerID)
            if err != nil {
                return fmt.Errorf("fetch customer %s: %w", order.CustomerID, err)
            }
            order.CustomerName = customer.Name
            return nil
        })
    }

    return g.Wait()
}

gRPC Service

// internal/grpc/orders_server.go
type OrdersServer struct {
    pb.UnimplementedOrdersServiceServer
    db     *pgxpool.Pool
    logger *slog.Logger
}

func (s *OrdersServer) CreateOrder(
    ctx context.Context,
    req *pb.CreateOrderRequest,
) (*pb.CreateOrderResponse, error) {
    if req.CustomerId == "" {
        return nil, status.Error(codes.InvalidArgument, "customer_id is required")
    }

    order, err := createOrderInDB(ctx, s.db, req)
    if err != nil {
        s.logger.ErrorContext(ctx, "failed to create order", "error", err)
        return nil, status.Error(codes.Internal, "internal error")
    }

    return &pb.CreateOrderResponse{OrderId: order.ID}, nil
}

func (s *OrdersServer) StreamOrders(
    req *pb.StreamOrdersRequest,
    stream pb.OrdersService_StreamOrdersServer,
) error {
    ctx := stream.Context()

    rows, err := s.db.Query(ctx,
        `SELECT id, status, total_cents FROM orders WHERE customer_id = $1`,
        req.CustomerId,
    )
    if err != nil {
        return status.Errorf(codes.Internal, "query failed: %v", err)
    }
    defer rows.Close()

    for rows.Next() {
        var order pb.Order
        if err := rows.Scan(&order.Id, &order.Status, &order.TotalCents); err != nil {
            return status.Errorf(codes.Internal, "scan failed: %v", err)
        }
        if err := stream.Send(&order); err != nil {
            return err
        }
    }

    return rows.Err()
}

// Start gRPC server alongside HTTP
func startGRPCServer(logger *slog.Logger, db *pgxpool.Pool) {
    lis, err := net.Listen("tcp", ":9090")
    if err != nil {
        logger.Error("gRPC listen failed", "error", err)
        os.Exit(1)
    }

    s := grpc.NewServer(
        grpc.ChainUnaryInterceptor(
            loggingUnaryInterceptor(logger),
        ),
    )
    pb.RegisterOrdersServiceServer(s, &OrdersServer{db: db, logger: logger})
    reflection.Register(s)  // For grpcurl debugging

    slog.Info("gRPC server starting", "addr", ":9090")
    if err := s.Serve(lis); err != nil {
        logger.Error("gRPC serve failed", "error", err)
    }
}

Structured Logging Middleware

func structuredLogger(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
            next.ServeHTTP(ww, r)
            logger.InfoContext(r.Context(), "request",
                "method", r.Method,
                "path", r.URL.Path,
                "status", ww.Status(),
                "bytes", ww.BytesWritten(),
                "duration_ms", time.Since(start).Milliseconds(),
                "request_id", middleware.GetReqID(r.Context()),
            )
        })
    }
}

Table-Driven Tests

// orders_test.go
func TestCreateOrder(t *testing.T) {
    tests := []struct {
        name       string
        req        CreateOrderRequest
        wantStatus int
        wantErr    string
    }{
        {
            name:       "valid order",
            req:        CreateOrderRequest{CustomerID: "cust-1", TotalCents: 1000},
            wantStatus: http.StatusCreated,
        },
        {
            name:       "missing customer ID",
            req:        CreateOrderRequest{TotalCents: 1000},
            wantStatus: http.StatusUnprocessableEntity,
            wantErr:    "customer_id is required",
        },
        {
            name:       "zero amount",
            req:        CreateOrderRequest{CustomerID: "cust-1", TotalCents: 0},
            wantStatus: http.StatusUnprocessableEntity,
            wantErr:    "total_cents must be positive",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            body, _ := json.Marshal(tt.req)
            req := httptest.NewRequest(http.MethodPost, "/api/v1/orders", bytes.NewReader(body))
            req.Header.Set("Content-Type", "application/json")
            w := httptest.NewRecorder()

            handler.createOrder(w, req)

            assert.Equal(t, tt.wantStatus, w.Code)
            if tt.wantErr != "" {
                var resp map[string]string
                json.NewDecoder(w.Body).Decode(&resp)
                assert.Contains(t, resp["error"], tt.wantErr)
            }
        })
    }
}

For the Protocol Buffers definitions used by Go gRPC services, see the Protocol Buffers guide. For adding Dapr to Go microservices for pub/sub and state management, the Dapr guide shows the sidecar pattern. The Claude Skills 360 bundle includes Go skill sets covering HTTP/gRPC services, goroutine patterns, and structured logging. Start with the free tier to try Go microservice generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free