Building a Web Server
Learn how to build a basic HTTP server in Go using net/http, http.HandleFunc, http.ListenAndServe, and a small JSON API endpoint.
Introduction
One of Go's most loved strengths is that a production-capable HTTP server is just a standard library import away — no framework required. The net/http package gives you routing, request parsing, and a concurrent server (every request is handled in its own goroutine) right out of the box.
- The core pieces of the net/http package.
- How to register routes and handlers with http.HandleFunc.
- How to start listening for requests with http.ListenAndServe.
- How to build a small JSON API endpoint end to end.
net/http Basics
At the center of net/http are two interfaces you will meet constantly: http.ResponseWriter, which you write your response to, and *http.Request, which describes the incoming request — its method, URL, headers, and body.
func handler(w http.ResponseWriter, r *http.Request) { // w: write the response here // r: read the request here (r.Method, r.URL.Path, r.Body, ...)}Registering Routes with HandleFunc
http.HandleFunc registers a function to handle requests for a given URL path. Since Go 1.22, patterns can also include the HTTP method and path variables, e.g. "GET /users/{id}".
package main
import ( "fmt" "net/http")
func homeHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Welcome to the PrograMinds API")}
func healthHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "OK")}
func main() { http.HandleFunc("/", homeHandler) http.HandleFunc("/health", healthHandler)
fmt.Println("server starting on :8080")}Starting the Server with ListenAndServe
http.ListenAndServe starts the server, binds to the given address, and blocks forever handling incoming requests (each on its own goroutine) until an unrecoverable error occurs, such as the port already being in use.
package main
import ( "fmt" "log" "net/http")
func homeHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Welcome to the PrograMinds API")}
func main() { http.HandleFunc("/", homeHandler)
fmt.Println("server starting on :8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) }}Click Run to see what this code prints.
Run this with go run main.go, then visit http://localhost:8080 in a browser or run curl http://localhost:8080 in another terminal to see the response.
Building a JSON API Endpoint
Combining what you learned about JSON in the previous lesson with net/http, here is a small endpoint that accepts a POST request with a JSON body and responds with a JSON result.
package main
import ( "encoding/json" "log" "net/http")
type GreetRequest struct { Name string `json:"name"`}
type GreetResponse struct { Message string `json:"message"`}
func greetHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return }
var req GreetRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON body", http.StatusBadRequest) return }
resp := GreetResponse{Message: "Hello, " + req.Name + "!"}
w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(resp)}
func main() { http.HandleFunc("/greet", greetHandler)
log.Println("server starting on :8080") log.Fatal(http.ListenAndServe(":8080", nil))}curl -X POST http://localhost:8080/greet \ -H "Content-Type: application/json" \ -d '{"name":"Gopher"}'Click Run to see what this code prints.
Common Mistakes
- Forgetting to set Content-Type: application/json before writing a JSON response.
- Not checking r.Method, so a handler meant for POST also silently accepts GET.
- Ignoring the error from json.NewDecoder(...).Decode, letting malformed bodies crash logic downstream.
- Calling w.WriteHeader after already writing to the response body — headers must come first.
- Using http.ListenAndServe(":8080", nil) in production without timeouts, leaving the server open to slow-client attacks.
Best Practices
- Validate the HTTP method and request body before doing any real work in a handler.
- Use http.Error for consistent error responses with the correct status code.
- Set Content-Type explicitly for JSON responses.
- For production servers, construct an http.Server with explicit ReadTimeout and WriteTimeout instead of the bare ListenAndServe.
- Keep handler functions small — extract business logic into separate, testable functions.
Frequently Asked Questions
No. net/http alone is enough for many production services, especially since Go 1.22 added method- and pattern-based routing. Frameworks add convenience for larger projects but are not required to get started.
Yes. http.ListenAndServe automatically handles each incoming request in its own goroutine, so your server is concurrent by default without any extra code.
With Go 1.22+, register a pattern like "GET /users/{id}" and read it inside the handler with r.PathValue("id"). Earlier versions typically parsed r.URL.Path manually or used a router library.
Key Takeaways
- http.ResponseWriter and *http.Request are the two core pieces of every handler.
- http.HandleFunc registers a function to handle requests matching a given pattern.
- http.ListenAndServe starts the server and blocks, handling each request on its own goroutine.
- json.NewDecoder and json.NewEncoder stream JSON directly to and from an HTTP request/response.
- Always validate method and body, and set proper status codes and headers.
Summary
With just the standard library, you can stand up a fully concurrent HTTP server that speaks JSON — no external framework required. Now that you can build features, the next lesson covers how to verify they actually work: testing in Go.