Go's net/http package is production-grade out of the box. DockerWhat is docker?A tool that packages your application and all its dependencies into a portable container that runs identically on any machine., Kubernetes, and countless microservicesWhat is microservices?An architecture where an application is split into small, independently deployed services that communicate over the network, each owning its own data. run on it without any framework. When you ask AI to build an HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. server, it generates working code fast, but with subtle security gaps that won't show up until production traffic hits.
Your job: recognize what's missing from AI-generated servers.
The AI-generated server vs the production server
When you ask AI to build a server, you get this:
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}This works for demos. Here's what production needs:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("POST /api/users", createUser)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Printf("Server starting on %s", server.Addr)
log.Fatal(server.ListenAndServe())
}http.ListenAndServe(":8080", nil) which has zero timeouts. A slow client can hold a connection open forever, eventually exhausting your server's file descriptors. Always use http.Server{} with explicit timeouts. This is the single most common security gap in AI-generated Go servers.What each timeout prevents
| Timeout | Default | What it prevents |
|---|---|---|
ReadTimeout | None (infinite) | Slowloris attacks, slow request sends |
WriteTimeout | None (infinite) | Clients that never read the response |
IdleTimeout | None (infinite) | Connections held open doing nothing |
ReadHeaderTimeout | None | Slow header sends (more targeted than ReadTimeout) |
Routing with Go 1.22+
Since Go 1.22, the built-in router handles method matching and path parameters. This is what AI should generate:
mux := http.NewServeMux()
// Method-specific routes
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// Wildcard (catch remaining path)
mux.HandleFunc("GET /files/{path...}", serveFile)Extracting path parameters
func getUser(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
// ... fetch user by id
}switch r.Method and manual path parsing. If you're on Go 1.22+, this is unnecessary complexity. Tell AI your Go version explicitly.Request handling patterns
Reading query parameters
func searchHandler(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
if q == "" {
http.Error(w, "Missing search query", http.StatusBadRequest)
return
}
page := r.URL.Query().Get("page")
if page == "" {
page = "1"
}
}Reading JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. request bodies
func createUser(w http.ResponseWriter, r *http.Request) {
// ALWAYS limit body size
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if user.Name == "" || user.Email == "" {
http.Error(w, "name and email required", http.StatusBadRequest)
return
}
// ... create user
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}defer r.Body.Close() in HTTP handlers. The server automatically closes request bodies, you don't need to, and it's not wrong, but it's a sign AI is confusing client-side patterns (where you DO close resp.Body) with server-side patterns.Writing responses
| Method | Purpose | Order matters? |
|---|---|---|
w.Header().Set(k, v) | Set response header | Must be before WriteHeader/Write |
w.WriteHeader(code) | Set status code | Must be before Write |
w.Write([]byte) | Write body bytes | Implicitly sends 200 if no WriteHeader |
json.NewEncoder(w).Encode(v) | Write JSON body | Calls Write internally |
http.Error(w, msg, code) | Send error response | Sets Content-Type to text/plain |
w.WriteHeader() after w.Write() or after json.NewEncoder(w).Encode(). Once you write to the body, the status code is already sent (defaults to 200). The second WriteHeader call logs a warning and is ignored. Set headers and status code BEFORE writing the body.MiddlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.
Middleware in Go is a function that takes an http.Handler and returns an http.Handler. No framework magic needed.
func loggingMiddleware(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))
})
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v", err)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}Chaining middleware
// Order matters: outermost runs first
var handler http.Handler = mux
handler = loggingMiddleware(handler)
handler = corsMiddleware(handler)
handler = recoveryMiddleware(handler) // Outermost - catches all panicsAccess-Control-Allow-Origin: * which allows any domain. For production APIs with credentials (cookies, auth headers), you need to whitelist specific origins. The wildcard also doesn't work with credentials: 'include' on the frontend, the browser blocks it.Complete production server pattern
When you ask AI to build a RESTWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources. APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses., evaluate the result against this pattern:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
handler := recoveryMiddleware(loggingMiddleware(corsMiddleware(mux)))
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Fatal(server.ListenAndServe())
}Production checklist for AI-generated servers
| Check | Why it matters |
|---|---|
Uses http.Server{} with timeouts | Prevents connection exhaustion |
Body size limited with MaxBytesReader | Prevents memory exhaustion |
| Recovery middleware installed | Prevents one panic from killing server |
| CORS configured for specific origins | Prevents credential leakage |
| JSON errors return proper status codes | Prevents silent failures |
| Path parameters validated and converted | Prevents injection / zero-value bugs |