An HTTP Middleware in Go (Without Any Framework)
This post explains a minimal HTTP server written in pure Go, using only the standard library. The goal is to demonstrate how to implement middleware-like without using any third party library like Gin, Echo, or Chi.
High-Level Architecture
- A static backend registry (backendList)
- A route lookup function
- A custom middleware
- Plain http.HandlerFunc handlers
- The standard net/http multiplexer
No reflection, no dependency injection, no magic.
Backend Definition
type Backend struct {
Path string
isAuthRequired bool
Handle http.HandlerFunc
}
Each backend explicitly declares:
- Path: the HTTP path
- isAuthRequired: whether authentication is enforced
- Handle: the actual request handler
This structure replaces framework routing tables and keeps behavior explicit.
Backend Registry
var backendList []Backend = []Backend{
{Path: "/", isAuthRequired: false, Handle: HandleHome},
{Path: "/admin", isAuthRequired: true, Handle: HandleAdmin},
}
This is a static routing table.
Key properties:
- Routes are declared once
- Authentication rules are colocated with the route
- No runtime mutation
- Easy to reason about during code review
Route Lookup Logic
func backendByPath(path string) *Backend {
for i := range backendList {
if backendList[i].Path == path {
return &backendList[i]
}
}
return nil
}
This performs a linear scan to find the backend for the current path.
Middleware Implementation
func middleware(handle http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
This is classic Go middleware, it wraps another http.HandlerFunc and it intercepts the request before passing control downstream
Route Validation
backendNow := backendByPath(r.URL.Path)
if backendNow == nil {
w.WriteHeader(404)
fmt.Fprintf(w, `{"message": "Not Found"}`)
return
}
Even though the handler is registered, the middleware still validates the route.
This allows centralized control and consistent error responses.
Validating Authentication (api-key)
if backendNow.isAuthRequired {
auth := r.Header.Get("api-key")
if auth != "secret123" {
w.WriteHeader(401)
fmt.Fprintf(w, `{"message": "Unauthorized"}`)
return
}
}
Authentication is:
- Header-based
- Stateless
- Route-specific
This mimics what real API gateways do, but without JWTs or OAuth complexity.
Request Timing and Logging
start := time.Now()
w.Header().Set("Content-Type", "application/json")
ending := time.Since(start)
log.Printf(`request to "%s" executed in "%s".`, r.URL.Path, &ending)
This section demonstrates:
- Request lifecycle instrumentation
- Centralized logging
- Header normalization
HTTP Handlers
Home Handler
func HandleHome(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(201)
fmt.Fprintf(w, `{"message": "Hello, World to Home!"}`)
}
Simple response, no method branching, no authentication.
Admin Handler
func HandleAdmin(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
w.WriteHeader(200)
fmt.Fprintf(w, `{"message": "Hello, World to Admin"}`)
case "POST":
w.WriteHeader(201)
fmt.Fprintf(w, `{"message": "Create the World to Admin"}`)
default:
w.WriteHeader(405)
fmt.Fprintf(w, `{"message": "Method Not Allowed"}`)
}
}
Key points:
- Explicit method handling
- No helper functions (educational purposes)
- Full control over HTTP semantics
- Of course you should validate body (educational purposes)
Server Bootstrap
func main() {
for _, backend := range backendList {
log.Printf(
"Registering backend auth required: %5t, at path: %s",
backend.isAuthRequired,
backend.Path,
)
http.HandleFunc(backend.Path, middleware(backend.Handle))
}
log.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}
This loop dynamically registers:
- Each backend
- Wrapped with the middleware
- On the default http.ServeMux
This is effectively a poor man’s router + middleware chain, built using only the standard library.
Why This Design Works
Advantages:
- Zero dependencies
- Transparent control flow
- Easy to debug
- Excellent for learning HTTP internals
Limitations:
- No path parameters
- Linear route lookup
- Manual error handling
- No middleware chaining
Again, this is just for educational purposes in how to fastly execute things in Go. That’s the real value here.
Full Code
package main
import (
"fmt"
"log"
"net/http"
"time"
)
type Backend struct {
Path string
isAuthRequired bool
Handle http.HandlerFunc
}
var backendList []Backend = []Backend{
{Path: "/", isAuthRequired: false, Handle: HandleHome},
{Path: "/admin", isAuthRequired: true, Handle: HandleAdmin},
}
func backendByPath(path string) *Backend {
for i := range backendList {
if backendList[i].Path == path {
return &backendList[i]
}
}
return nil
}
func middleware(handle http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
backendNow := backendByPath(r.URL.Path)
if backendNow == nil {
w.WriteHeader(404)
fmt.Fprintf(w, `{"message": "Not Found"}`)
return
}
if backendNow.isAuthRequired {
auth := r.Header.Get("api-key")
if auth != "secret123" {
w.WriteHeader(401)
fmt.Fprintf(w, `{"message": "Unauthorized"}`)
return
}
}
start := time.Now()
w.Header().Set("Content-Type", "application/json")
ending := time.Since(start)
log.Printf(`request to "%s" executed in "%s".`, r.URL.Path, &ending)
handle.ServeHTTP(w, r)
})
}
func HandleHome(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(201)
fmt.Fprintf(w, `{"message": "Hello, World to Home!"}`)
}
func HandleAdmin(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
fmt.Fprintf(w, `{"message": "Hello, World to Admin"}`)
w.WriteHeader(200)
return
case "POST":
fmt.Fprintf(w, `{"message": "Create the World to Admin"}`)
w.WriteHeader(201)
return
default:
fmt.Fprintf(w, `{"message": "Method Not Allowed"}`)
w.WriteHeader(405)
return
}
}
func main() {
for _, backend := range backendList {
log.Printf("Registering backend auth required: %5t, at path: %s", backend.isAuthRequired, backend.Path)
http.HandleFunc(backend.Path, middleware(backend.Handle))
}
log.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}