Routing: Multiple Endpoints & Methods


Progress
0/3

🎯 Your mission: Lesson 0001 gave you one route. Real APIs need many β€” GET /users, POST /users, GET /users/42. This lesson teaches routing by path and HTTP method.

What you’ll build: A server with three endpoints:

  • GET / β†’ welcome message
  • GET /health β†’ {"status":"ok"}
  • POST /echo β†’ reads request body, sends it back

Pointers: what *http.Request means

The * is a pointer β€” instead of copying the Request, Go passes a memory address. For now: pointer = β€œdon’t copy, just point to the original.” β†’ Tour of Go: Pointers

http.NewServeMux β€” your own router

In lesson 0001 we passed nil to ListenAndServe. That uses a hidden default router. Better: create your own ServeMux and pass it explicitly. β†’ ServeMux docs

Checking r.Method

HandleFunc matches on path, not method. To restrict to GET, POST, etc., check r.Method inside the handler. Go provides constants: http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete. β†’ method constants

Request: GET /health
↓
ServeMux checks registered paths
↓
Matches "/health" β†’ calls healthHandler(w, r)

Follow each step in order. Click Done βœ“ after completing it to unlock the next one.

1Create directory and initialize moduleCurrent
mkdir -p ~/dev/learn/go-lang/lessons/0002-routing
cd ~/dev/learn/go-lang/lessons/0002-routing
go mod init routing-server
2Write the multi-route serverLocked

Create main.go:

package main

import (
  "io"
  "log"
  "net/http"
)

func welcomeHandler(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Welcome to the routing server!\n"))
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "application/json")
  w.Write([]byte(`{"status":"ok"}`))
}

func echoHandler(w http.ResponseWriter, r *http.Request) {
  if r.Method != http.MethodPost {
      http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
      return
  }
  body, err := io.ReadAll(r.Body)
  if err != nil {
      http.Error(w, "Failed to read body", http.StatusBadRequest)
      return
  }
  defer r.Body.Close()
  w.Write(body)
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", welcomeHandler)
  mux.HandleFunc("/health", healthHandler)
  mux.HandleFunc("/echo", echoHandler)

  log.Println("Server starting on http://localhost:8080")
  log.Fatal(http.ListenAndServe(":8080", mux))
}
What’s new here?
  • mux := http.NewServeMux() β€” explicit router, no more nil
  • w.Header().Set(…) β€” set response headers
  • r.Method != http.MethodPost β€” reject wrong methods
  • io.ReadAll(r.Body) β€” read the request body
  • defer r.Body.Close() β€” cleanup when the function returns
  • http.Error(w, msg, code) β€” send error response with status code
3Run and test all endpointsLocked

Start the server:

go run main.go

In a second terminal, test each endpoint:

# GET /
curl http://localhost:8080/

# GET /health
curl http://localhost:8080/health

# POST /echo
curl -X POST -d "hello world" http://localhost:8080/echo

# GET /echo (should return 405 Method Not Allowed)
curl http://localhost:8080/echo

Press Ctrl+C to stop the server when you’re done.

Check your understanding

Why pass mux to ListenAndServe instead of nil?

A request comes in as PUT /echo. What happens?

What does defer r.Body.Close() do?