HTTP and APIs — Junior¶
At junior level, focus on this question:
How can I apply HTTP and APIs in one small example and prove the result?
Use the smallest realistic scenario that exposes the decision and its failure behavior.¶
Core Concepts¶
1. A handler is just a function with a specific signature¶
w is how you write the response; r carries the request's method, URL, headers, and body.
2. ServeMux routes by path¶
mux := http.NewServeMux()
mux.HandleFunc("/users", listUsers)
mux.HandleFunc("/users/", getUser) // trailing slash matches a subtree in older Go
Go 1.22+ added method- and pattern-based routing directly to ServeMux (mux.HandleFunc("GET /users/{id}", handler)), removing the need for a third-party router for many common cases.
3. Middleware wraps handlers¶
func withLogging(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 %v", r.Method, r.URL.Path, time.Since(start))
})
}
Wrap once, apply everywhere: mux becomes withLogging(mux).
4. Never use http.ListenAndServe bare in production¶
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
srv.ListenAndServe()
Without explicit timeouts, a client that opens a connection and sends data slowly (or never finishes) can hold a goroutine and its resources indefinitely — a classic "Slowloris"-style resource-exhaustion risk. http.Server's zero-value timeouts are unlimited.
5. The request carries a context.Context you should respect¶
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := doWork(ctx) // pass it down
...
}
If the client disconnects or the server shuts down, r.Context() is canceled — any downstream call that respects it (a database query, an outbound HTTP call) can stop early instead of doing wasted work.
6. Always close response bodies on the client side¶
Forgetting resp.Body.Close() leaks the underlying connection, eventually exhausting the client's connection pool.
Code Examples¶
Example 1 — A server with sane defaults¶
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Fatal(srv.ListenAndServe())
Example 2 — JSON request/response¶
type CreateUserReq struct{ Name string `json:"name"` }
func createUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"name": req.Name})
}
Example 3 — A client with a timeout¶
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("https://example.com")
if err != nil { return err }
defer resp.Body.Close()
The zero-value http.Client{} (or the package-level http.Get) has no timeout — always set one explicitly.
Best Practices¶
- Always set
ReadTimeout,WriteTimeout(orReadHeaderTimeoutat minimum), andIdleTimeoutonhttp.Server. - Always set
Timeoutonhttp.Client; never use the zero-value client for real traffic. - Always
defer resp.Body.Close()after checking the error from an HTTP client call. - Pass
r.Context()down to anything the handler calls that might block. - Return meaningful status codes (
400for bad input,404for not found,500for server errors) — don't return200with an error message in the body.
Edge Cases & Pitfalls¶
- Forgetting
resp.Body.Close()leaks connections even if you never read the body. - A handler that panics with no recovery middleware crashes the entire server process by default (unless behind
http.Server's own per-connection recovery, which only prevents that one connection from taking down others, but still returns a broken connection to that client). - Reading
r.Bodywithout a size limit lets a malicious client send an unbounded body — usehttp.MaxBytesReaderfor anything public-facing.
Common Mistakes¶
| Mistake | Fix |
|---|---|
http.ListenAndServe(":8080", nil) in production | Use an http.Server with explicit timeouts |
Zero-value http.Client{} for outbound calls | Set an explicit Timeout |
Ignoring r.Context() | Pass it to every downstream call that can block |
| No body size limit on public endpoints | Wrap with http.MaxBytesReader |
Apply it¶
- Choose one small, known input for HTTP and APIs.
- Predict the output or observable behavior.
- Run the smallest example or probe that exercises the concept.
- Change one input to trigger a failure or boundary case.
- Explain the evidence using the guide's vocabulary.
Verify your work¶
- Record the exact input, command or code path, and output.
- Repeat the probe and confirm the result is consistent.
- Show one expected success and one expected failure.
- Resolve any difference between the prediction and the evidence.
Review questions¶
- What problem does HTTP and APIs solve in the example?
- Which input changes the observed result, and why?
- What is the smallest useful success check?
- Which beginner mistake would your evidence catch?
In this topic
- junior
- middle
- senior
- professional