Learn / Go Service Engineering
HTTP Service Basics
Build a small JSON API with standard handlers, structured errors, timeouts, and a simple project layout.
Course: Go Service Engineering. Level: Intermediate. Topic: Reliable services.
Stage: basic - Basic service contracts - Go HTTP service contracts. Start with handlers that expose context, JSON boundaries, timeouts, and structured errors clearly.
Outcomes
- Create context-aware handlers.
- Decode and encode JSON safely.
- Configure server timeouts.
- Build HTTP handlers using net/http.
- Decode requests and encode responses safely.
- Add middleware for cross-cutting concerns.
Concepts
- http.Handler
- ServeMux
- JSON decoder
- server timeout
- net/http and Services
- Handlers are small adapters
- Decode defensively
- Middleware wraps handlers
- Go Programming foundations
- Guided practice
Concept flow
Show how go http service contracts moves from trigger to implementation outcome in Go Services.
- Client
- ServeMux
- Handler
- Service
- JSON response
Session flow
- Model http.Handler (concept, 8 min) — Name the decisions behind http.Handler before writing code.
- Create context-aware handlers.
- Explain where http.Handler belongs in ticket processing platform.
- Build the vertical slice (walkthrough, 15 min) — Implement the smallest useful slice in cmd/api/main.go.
- Decode and encode JSON safely.
- Connect ServeMux to a working example.
- Verify and harden (exercise, 10 min) — Add read and write timeouts.
- Configure server timeouts.
- Record one risk or follow-up before moving on.
- Handlers, JSON, and Middleware: Handlers are small adapters (concept, 32 min) — An HTTP handler should adapt the network protocol to application behavior. Keep request parsing, response writing, and status code choices near the handler, but move business logic into ordinary Go types that are easy to test without a server.
- Retained source example: Basic JSON handler
func Health(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", Health)
log.Fatal(http.ListenAndServe(":8080", mux))
}
Modern ServeMux patterns can include method and path.
- Handlers, JSON, and Middleware: Decode defensively (walkthrough, 32 min) — Limit request body size when appropriate, disallow unknown fields for strict APIs, validate decoded data, and return consistent JSON errors. Do not let transport concerns leak deep into domain logic.
- Retained source example: Strict JSON decode
func decodeCreateUser(r *http.Request) (CreateUserRequest, error) {
var req CreateUserRequest
dec := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
return req, err
}
return req, nil
}
In a real handler, pass the actual ResponseWriter to MaxBytesReader so the server can manage the connection correctly.
- Handlers, JSON, and Middleware: Middleware wraps handlers (walkthrough, 32 min) — Middleware is a function that takes an http.Handler and returns another http.Handler. Use it for logging, request IDs, authentication, panic recovery, compression, or timeouts. Keep middleware order intentional.
- Retained source example: Logging middleware
func LogRequests(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
Middleware should call next exactly once for ordinary pass-through behavior.
- Handlers, JSON, and Middleware: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Keep handlers thin and domain logic testable without HTTP.
- Practice: Set response headers before writing the body.
- Practice: Use httptest for handler behavior and status code tests.
- Avoid: Writing response bodies before setting the status code.
- Avoid: Ignoring JSON encoder or decoder errors in important paths.
- Avoid: Putting all business logic directly inside handlers.
- Handlers, JSON, and Middleware: references (review, 2 min) — Original references retained from the legacy library.
- Package net/http: https://pkg.go.dev/net/http
- Package net/http/httptest: https://pkg.go.dev/net/http/httptest
Code example
Go in cmd/api/main.go.
package main
import (
"log/slog"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /tickets", createTicket)
server := &http.Server{
Addr: ":8080",
Handler: requestLogger(mux),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
slog.Info("api listening", "addr", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("api stopped", "error", err)
}
}
func createTicket(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
}
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.InfoContext(r.Context(), "request", "method", r.Method, "path", r.URL.Path)
next.ServeHTTP(w, r)
})
}
Walkthrough examples
- HTTP Service Basics in a ticket processing platform — A team is extending a small Go service plus worker and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: cmd/api/main.go
- File: tests/http-service-basics.spec
- File: docs/go-services/http-service-basics.md
- Start from the provided Go snippet and make the intent visible in names and boundaries.
- Apply the checklist item "Use request context" before adding extra behavior.
- Write down how the implementation changes when ServeMux fails or becomes slow.
- Basic JSON handler — Modern ServeMux patterns can include method and path.
- Retained source code:
func Health(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", Health)
log.Fatal(http.ListenAndServe(":8080", mux))
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Strict JSON decode — In a real handler, pass the actual ResponseWriter to MaxBytesReader so the server can manage the connection correctly.
- Retained source code:
func decodeCreateUser(r *http.Request) (CreateUserRequest, error) {
var req CreateUserRequest
dec := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
return req, err
}
return req, nil
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Logging middleware — Middleware should call next exactly once for ordinary pass-through behavior.
- Retained source code:
func LogRequests(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Handler test — httptest lets you test handlers without opening a real network port.
- Retained source code:
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rr := httptest.NewRecorder()
Health(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status = %d", rr.Code)
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Create one POST handler.
- Return a structured error for invalid JSON.
- Add read and write timeouts.
- Name the handler method: What method must a type implement to satisfy http.Handler?
- Hint: The method receives a ResponseWriter and a Request pointer.
- Reference solution: A type satisfies http.Handler by implementing ServeHTTP(http.ResponseWriter, *http.Request).
- Accepted answers: ServeHTTP | servehttp | ServeHTTP(ResponseWriter, *Request)
Checklist
- Use request context
- Limit request body size
- Set server timeouts
- Return consistent JSON errors
Quiz prompts
- Why set HTTP server read and write timeouts? — Timeouts protect service resources when clients are slow or connections stall.
- A teammate wants to hide http.Handler inside a convenient helper. What should you check first? — Place http.Handler at the boundary that keeps ticket processing platform behavior explicit, testable, and reviewable.
- Which artifact best proves this Go Services lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Basic service contracts: a teammate says the happy path works, but "Handler contract" is still implicit. What should you ask for before merging? — Handler contract belongs in the basic stage only when the decision is visible, testable, and tied to a realistic failure mode.
- A reviewer has five minutes to evaluate this basic Go Services slice. Which evidence is strongest? — Build the basic Go service shell: one typed JSON endpoint, server timeouts, request limits, and a consistent error envelope.
- What package helps test handlers without a real server port? — httptest provides request and response recorder helpers.
- What shape does middleware usually have? — Middleware wraps one handler with another.
Flashcards
- Basic service contracts: what decision does "Handler contract" force you to make? Make each handler own decoding, validation, service delegation, and response encoding explicitly. Evidence prompt: Write one POST handler with request size limits and a typed error response.
- Basic service contracts: what decision does "Timeout defaults" force you to make? Protect the process from slow clients before the service handles real traffic. Evidence prompt: Add server timeouts and note what each timeout prevents.
- Basic service contracts: what decision does "Error envelope" force you to make? Return consistent JSON errors so clients do not parse ad hoc strings. Evidence prompt: Replace one plain-text error with a structured JSON error contract.
- In Go Services, what should you remember about http.Handler? http.Handler matters here because it supports "Create context-aware handlers.".
- In Go Services, what should you remember about ServeMux? ServeMux matters here because it supports "Decode and encode JSON safely.".
- In Go Services, what should you remember about JSON decoder? JSON decoder matters here because it supports "Configure server timeouts.".
- In Go Services, what should you remember about server timeout? server timeout matters here because it supports "Create context-aware handlers.".
Labs
- Ship a http service basics slice — Extend a small Go service plus worker with a small but reviewable feature that proves the lesson's architecture in code.
- Build the basic Go service shell: one typed JSON endpoint, server timeouts, request limits, and a consistent error envelope.
- Write one POST handler with request size limits and a typed error response.
- Add server timeouts and note what each timeout prevents.
- Replace one plain-text error with a structured JSON error contract.
- Create one POST handler.
- Return a structured error for invalid JSON.
- The lab demonstrates the basic service contracts outcome without skipping earlier contract evidence.
- Each subtopic has a concrete artifact: Handler contract, Timeout defaults, Error envelope.
- The review notes explain how this level changes ownership, verification, or operational risk.
Challenge
- Review-ready http service basics (Core) — Build the basic Go service shell: one typed JSON endpoint, server timeouts, request limits, and a consistent error envelope.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from cmd/api/main.go plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL