f in x
HTTP Server in Go — Routing and Middleware with net/http for Production APIs
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

HTTP Server in Go — Routing and Middleware with net/http for Production APIs

[2026-06-22] Author: Ing. Calogero Bono

Why does your Go HTTP server need proper routing and middleware?

You started with http.HandleFunc and everything worked. Then came endpoints with parameters, authentication, logging, centralized error handling. The code turned into a mess of if-else chains, duplicate logic, and copy-paste. The problem is not Go: it's using the right tool the wrong way.

We at Meteora Web have been using Go for years on custom backend platforms. We started simple too — then we realized a well-structured HTTP server is the difference between a project that stays maintainable and one that becomes a cost every time you need to add a route.

This guide shows you how to build a routing and middleware system that scales, using only the Go standard library (net/http). No external frameworks, no unnecessary dependencies. Just code you understand and control.

How does the net/http multiplexer work in Go?

The heart of every Go HTTP server is http.ServeMux, a multiplexer that maps URL patterns to handlers. Until Go 1.21, patterns were fixed: "/api/users" or "/api/" for prefixes. No dynamic parameters like :id.

With Go 1.22 (and later), the multiplexer supports patterns with parameters and HTTP methods. This changes everything: you can define routes like GET /users/{id} directly with the standard library, without external packages.

Sponsored Protocol

Basic example: ServeMux with modern patterns

package main

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

func main() {
	mux := http.NewServeMux()

	// Pattern with method and dynamic parameter
	mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		fmt.Fprintf(w, "User ID: %s", id)
	})

	mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "User created")
	})

	log.Fatal(http.ListenAndServe(":8080", mux))
}

Note: r.PathValue() is available from Go 1.22. If you're on older versions, you need packages like gorilla/mux or chi — but we recommend upgrading Go and using the standard library.

How to implement advanced routing with pattern matching in Go 1.22+?

The new ServeMux supports:

  • Explicit HTTP methods: GET /resource, POST /resource, PUT /resource/{id}.
  • Dynamic parameters: {id}, {slug}, {path*} to match multiple segments.
  • Priority based on specificity: more specific patterns win over generic ones.

Example: routing with multiple methods and parameters

mux.HandleFunc("GET /posts", listPosts)
mux.HandleFunc("POST /posts", createPost)
mux.HandleFunc("GET /posts/{slug}", getPost)
mux.HandleFunc("PUT /posts/{slug}", updatePost)
mux.HandleFunc("DELETE /posts/{slug}", deletePost)

Notice: you no longer need manual r.Method checks. The mux itself handles 405 Method Not Allowed if the method doesn't match.

Sponsored Protocol

Handling wildcard asterisk

Use {path*} to capture the rest of the path:

mux.HandleFunc("GET /files/{path*}", func(w http.ResponseWriter, r *http.Request) {
	path := r.PathValue("path")
	fmt.Fprintf(w, "Requested path: %s", path)
})

How to create composable middleware for authentication, logging and recovery?

A middleware in Go is a function that takes an http.Handler and returns another http.Handler. The standard signature is:

type Middleware func(http.Handler) http.Handler

You can compose middleware as nested functions. Here are the three most useful for a serious backend.

Logging middleware

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%s %s %s", r.Method, r.URL.Path, r.RemoteAddr)
		next.ServeHTTP(w, r)
	})
}

Authentication middleware (API Key)

func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		key := r.Header.Get("X-API-Key")
		if key != "super-secret-key" {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

Recovery middleware (panic catching)

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", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

Composing middleware

Use a helper function to apply multiple middleware in sequence:

Sponsored Protocol

func chainMiddleware(handler http.Handler, middlewares ...Middleware) http.Handler {
	for i := len(middlewares) - 1; i >= 0; i-- {
		handler = middlewares[i](handler)
	}
	return handler
}

Then apply to your mux:

mux := http.NewServeMux()
mux.Handle("GET /protected", chainMiddleware(
	http.HandlerFunc(protectedHandler),
	recoveryMiddleware,
	authMiddleware,
	loggingMiddleware,
))

Or, if you want global middleware for all routes, wrap the mux:

Sponsored Protocol

finalHandler := chainMiddleware(mux, recoveryMiddleware, loggingMiddleware)
log.Fatal(http.ListenAndServe(":8080", finalHandler))

How to handle context and decorator pattern for complex handlers?

Often you need to pass data (logged-in user, DB connection) from middleware to handler. In Go, use context.Context via r.Context().

Middleware enriching context

func contextMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// example: user extracted from token
		user := &User{ID: 42, Name: "Mario"}
		ctx := context.WithValue(r.Context(), "user", user)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

In the handler, retrieve the value:

func protectedHandler(w http.ResponseWriter, r *http.Request) {
	user := r.Context().Value("user").(*User)
	fmt.Fprintf(w, "Welcome %s", user.Name)
}

Warning: context is not a dumping ground for arbitrary data. Use it only for request-scoped values (auth, trace IDs, etc.).

What mistakes to avoid when building an HTTP server in Go with net/http?

We've seen plenty in projects that come to us. Here are the most common:

  • Not closing the body: always use defer r.Body.Close() in every handler that reads the body.
  • No timeouts: a server without timeouts will die. Set http.Server{ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second}.
  • Global middleware without exclusions: sometimes you need to bypass auth on health checks. Create an explicit route or use an exclusion pattern.
  • Ignoring ListenAndServe errors: http.ListenAndServe returns an error if the server crashes. Log it or panic in development.

What to do next

  1. Upgrade Go to 1.22+ if you haven't already. The new mux is a game changer.
  2. Rewrite your routes using patterns with method and parameters. Eliminate all manual r.Method switches.
  3. Create a middleware.go file with logging, recovery, and auth middleware. Experiment with composition.
  4. Test with httptest — the standard library has httptest.NewServer to test your handlers without starting a real server.
  5. For a deeper dive into Go for backend, check our main guide on Go for Backend: Concurrency, REST APIs and Microservices.

A well-built HTTP server with net/http gives you total control. No dependencies, no black magic. Just code that does exactly what you wrote. Just the way we like it.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()