Go Web Frameworks: net/http, Chi, Gin, Echo and Fiber · The Standard Library and the Big Four

Start with net/http: What the Standard Library Already Does

Since Go 1.22 the standard library router matches methods and path parameters, so many APIs need no framework at all. Build a JSON endpoint, add middleware, and learn the Handler interface every Go framework builds on.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A tiny "books" API with GET /books, GET /books/{id} and POST /books, JSON in and out, plus a logging middleware, using only the standard library. Every later lesson rebuilds this API with a framework so you can compare.

The idea

Everything in Go HTTP revolves around one interface:

type Handler interface {
	ServeHTTP(w http.ResponseWriter, r *http.Request)
}

http.HandlerFunc turns any function with that signature into a Handler. A middleware is just a function that takes a handler and returns a new handler wrapping it.

Since Go 1.22, http.ServeMux understands patterns like "GET /books/{id}": it matches the method, captures {id} (read it with r.PathValue("id")) and answers 405 for the wrong method. That covers much of what people used to install a router for.

What the standard library still leaves to you: route groups, request binding and validation, a central error handler, and convenience helpers for JSON. That's what frameworks add.

net/http/httptest lets you send requests to a handler in memory, which is how every example in this course shows real responses.

The code

A JSON API with the Go 1.22+ ServeMux and a middleware
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
)

type Book struct {
	ID     int    `json:"id"`
	Title  string `json:"title"`
	Author string `json:"author"`
}

type Store struct {
	mu    sync.Mutex
	books []Book
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(v)
}

func (s *Store) list(w http.ResponseWriter, r *http.Request) {
	s.mu.Lock()
	defer s.mu.Unlock()
	writeJSON(w, http.StatusOK, s.books)
}

func (s *Store) get(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id") // captured by the "{id}" pattern
	s.mu.Lock()
	defer s.mu.Unlock()
	for _, b := range s.books {
		if fmt.Sprint(b.ID) == id {
			writeJSON(w, http.StatusOK, b)
			return
		}
	}
	writeJSON(w, http.StatusNotFound, map[string]string{"error": "book not found"})
}

func (s *Store) create(w http.ResponseWriter, r *http.Request) {
	var b Book
	if err := json.NewDecoder(r.Body).Decode(&b); err != nil || b.Title == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "title is required"})
		return
	}
	s.mu.Lock()
	b.ID = len(s.books) + 1
	s.books = append(s.books, b)
	s.mu.Unlock()
	writeJSON(w, http.StatusCreated, b)
}

// logging is a middleware: it wraps a handler and returns a new one.
func logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Printf("-> %s %s\n", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
	})
}

func main() {
	store := &Store{books: []Book{{1, "The Go Programming Language", "Alan Donovan, Brian Kernighan"}}}
	mux := http.NewServeMux()
	mux.HandleFunc("GET /books", store.list)
	mux.HandleFunc("GET /books/{id}", store.get)
	mux.HandleFunc("POST /books", store.create)
	app := logging(mux)

	send := func(method, path, body string) {
		req := httptest.NewRequest(method, path, strings.NewReader(body))
		rec := httptest.NewRecorder()
		app.ServeHTTP(rec, req)
		out, _ := io.ReadAll(rec.Body)
		fmt.Printf("   %d %s", rec.Code, out)
		if len(out) == 0 || out[len(out)-1] != '\n' {
			fmt.Println()
		}
	}

	send("GET", "/books/1", "")
	send("POST", "/books", `{"title":"Learning Go","author":"Jon Bodner"}`)
	send("POST", "/books", `{"author":"nobody"}`)
	send("GET", "/books", "")
	send("GET", "/books/9", "")
	send("DELETE", "/books/1", "")   // wrong method: the mux answers 405 by itself
}

Output

-> GET /books/1
   200 {"id":1,"title":"The Go Programming Language","author":"Alan Donovan, Brian Kernighan"}
-> POST /books
   201 {"id":2,"title":"Learning Go","author":"Jon Bodner"}
-> POST /books
   400 {"error":"title is required"}
-> GET /books
   200 [{"id":1,"title":"The Go Programming Language","author":"Alan Donovan, Brian Kernighan"},{"id":2,"title":"Learning Go","author":"Jon Bodner"}]
-> GET /books/9
   404 {"error":"book not found"}
-> DELETE /books/1
   405 Method Not Allowed

Run this on your computer — see “Get Started” for setup.

Notice the last request: the mux knows /books/{id} exists for GET only, so it replies 405 Method Not Allowed (with an Allow header) without any code from you.

Common mistakes

  • Reaching for a framework by reflex. For small services, net/http + a few helpers is enough and has zero dependencies.
  • Forgetting to return after writing an error. The handler keeps running and writes a second response.
  • Writing headers after the body. w.Header().Set(...) must come before WriteHeader or the first Write.
  • Shared state without a lock. Handlers run concurrently; protect maps and slices with a mutex.
  • Using the old pattern syntax. Before Go 1.22, "/books/" was a prefix match and methods weren't matched; check your go.mod Go version.

Key points

  • http.Handler (ServeHTTP) is the interface every Go framework builds on or wraps.
  • Go 1.22+ ServeMux matches methods and path values: "GET /books/{id}" + r.PathValue("id").
  • Middleware = func(http.Handler) http.Handler.
  • Frameworks add groups, binding, validation and central error handling on top.

Further reading: net/http routing enhancements in Go 1.22, net/http package.

Practice questions

What changed in Go 1.22's http.ServeMux?

Answer

Patterns can include an HTTP method and wildcards, e.g. "GET /books/{id}"; the value is read with r.PathValue("id"). The mux returns 405 for a known path with the wrong method, and more specific patterns win over general ones.

What is a middleware in Go's net/http?

Answer

A function that takes an http.Handler and returns a new http.Handler that does something before and/or after calling the wrapped one (logging, auth, recovery). They compose by wrapping: logging(auth(mux)).

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?