Middleware en routing in Go: praktische handleiding

Leer hoe middleware en routing in Go werken. Van chi en gorilla/mux tot custom middleware chains met logging, auth en recovery, inclusief voorbeelden.

13 augustus 20267 min leestijdDoor We Develop Communication

Middleware en routing zijn twee bouwstenen die je Go webapplicatie structuur en flexibiliteit geven. Zodra je API groeit, merk je dat losse handlers niet meer volstaan, je wilt logging, authenticatie en error recovery op één plek regelen. In deze handleiding leer je hoe je routing opzet met de standaard library en populaire routers, en hoe je middleware chains bouwt die schoon en herbruikbaar zijn.

Deze gids bouwt voort op de basis uit HTTP servers bouwen in Go. Als je dat artikel nog niet gelezen hebt, is dat een goede start voordat je hier verder gaat.

Wat is routing in Go?

Routing is het proces waarbij een inkomend HTTP request wordt gekoppeld aan de juiste handler op basis van de URL en HTTP methode. In Go gebeurt dit standaard via net/http.ServeMux, maar voor productie-apps gebruik je vaak een externe router.

Een router beantwoordt vragen als: welke functie moet GET /users/42 afhandelen? Wat doe ik met POST /users? En hoe haal ik die 42 uit de URL?

ServeMux sinds Go 1.22

Sinds Go 1.22 is de ingebouwde http.ServeMux fors krachtiger geworden. Je kunt nu methodes en path parameters direct in het patroon specificeren.

package main

import (
    "fmt"
    "net/http"
)

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

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

    mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusCreated)
        fmt.Fprintln(w, "Gebruiker aangemaakt")
    })

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

Voor veel projecten is dit genoeg. Geen externe dependency, geen leercurve. Maar zodra je middleware wilt groeperen of sub-routers gebruikt, wordt het al snel onhandig.

Externe routers: chi en gorilla/mux

Twee populaire routers zijn chi en gorilla/mux. Beide zijn net/http compatibel, waardoor je bestaande handlers gewoon blijven werken.

Chi: lichtgewicht en idiomatisch

Chi is een minimale router die sterk leunt op de standaardinterfaces van Go. Hij voegt middleware chains, route groups en parameter parsing toe zonder je tot een eigen ecosysteem te dwingen.

import (
    "net/http"
    "github.com/go-chi/chi/v5"
)

func main() {
    r := chi.NewRouter()

    r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := chi.URLParam(r, "id")
        w.Write([]byte("ID: " + id))
    })

    http.ListenAndServe(":8080", r)
}

Gorilla/mux: volwassen maar gearchiveerd

Gorilla/mux was jarenlang de standaard, maar het project werd in 2022 gearchiveerd en in 2023 weer overgenomen door de community. Voor bestaande codebases is het prima, maar voor nieuwe projecten zou ik chi aanraden.

Wat is middleware?

Middleware is code die zich tussen het inkomende request en je handler bevindt. Denk aan een pijplijn: elk request passeert meerdere lagen voordat het je businesslogica bereikt.

Typische middleware taken zijn:

  • Logging, bijhouden welke requests binnenkomen en hoelang ze duren
  • Authenticatie, controleren of de gebruiker is ingelogd
  • Recovery, panics opvangen zodat je server niet crasht
  • CORS, headers toevoegen voor cross-origin requests
  • Rate limiting, misbruik voorkomen door request snelheid te beperken

Deze concepten bouwen voort op het begrip van functions en error handling, omdat middleware zwaar leunt op function composition.

Middleware in Go schrijven

In Go is middleware niets anders dan een functie met deze signatuur:

func(http.Handler) http.Handler

Het neemt een handler aan en retourneert een nieuwe handler die eerst iets extra's doet. Hier een eenvoudig logging voorbeeld:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

Je wikkelt hiermee je handler in een extra laag. Elk request passeert eerst loggingMiddleware, die tijd meet en doorgeeft aan de volgende handler via next.ServeHTTP.

Recovery middleware

Een panic in een handler kan normaal gesproken je hele server in gevaar brengen. Recovery middleware vangt dit op:

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

Deze middleware hoort bijna altijd bovenaan je chain, zodat álle andere middleware en handlers er door beschermd worden.

Authenticatie middleware

Voor beschermde routes wil je een middleware die het request afbreekt als er geen geldige token is:

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Niet geautoriseerd", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Let op de return na http.Error, zonder deze regel zou de handler alsnog worden uitgevoerd.

Middleware chains bouwen

De kracht van middleware komt uit het combineren van meerdere lagen. Met chi gaat dat zo:

r := chi.NewRouter()

r.Use(recoveryMiddleware)
r.Use(loggingMiddleware)

r.Group(func(r chi.Router) {
    r.Use(authMiddleware)
    r.Get("/dashboard", dashboardHandler)
    r.Post("/settings", settingsHandler)
})

r.Get("/public", publicHandler)

In dit voorbeeld krijgen álle routes recovery en logging. Alleen de routes binnen de Group krijgen ook authenticatie. De publieke route blijft toegankelijk zonder token.

Volgorde is belangrijk

De volgorde waarin je middleware registreert bepaalt de uitvoeringsvolgorde. Een veelgebruikte volgorde is:

  1. Recovery, vangt panics uit alle lagen erna op
  2. RequestID, voegt een uniek ID toe voor tracing
  3. Logging, logt met het request ID
  4. CORS, handelt preflight requests af
  5. Auth, controleert identiteit
  6. Rate limit, voorkomt misbruik per gebruiker

Zet je auth vóór logging, dan log je alleen geauthenticeerde requests, niet wat je meestal wilt.

Context doorgeven via middleware

Een elegante manier om data door te geven tussen middleware en handlers is via context.Context. Zo kun je bijvoorbeeld een geauthenticeerde user in de context stoppen:

type userKey struct{}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, err := parseToken(r.Header.Get("Authorization"))
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), userKey{}, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

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

Gebruik een custom type als key om collisies te voorkomen. Dit is een best practice die ook in de officiële Go documentatie wordt aangeraden.

Routing best practices

Enkele vuistregels die je codebase schoon houden naarmate je app groeit:

  • Groepeer routes per resource, alle user routes onder /users, alle order routes onder /orders
  • Gebruik sub-routers, chi's Route en Mount helpen je grote API's op te splitsen
  • Scheid handlers van routing, definieer handlers in aparte packages en registreer ze in een centrale routing functie
  • Versie je API, prefix met /v1 en /v2 zodat je kunt evolueren zonder clients te breken
  • Houd handlers dun, zet businesslogica in services, zodat handlers alleen HTTP afhandelen

Deze scheiding past naadloos in een goede project structuur, waar je handlers, services en repositories in aparte packages plaatst.

Testen van middleware en routes

Go's net/http/httptest package maakt het testen van routes en middleware direct uit de doos mogelijk:

func TestAuthMiddleware(t *testing.T) {
    handler := authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    }))

    req := httptest.NewRequest("GET", "/", nil)
    rec := httptest.NewRecorder()

    handler.ServeHTTP(rec, req)

    if rec.Code != http.StatusUnauthorized {
        t.Errorf("verwacht 401, kreeg %d", rec.Code)
    }
}

Dit maakt middleware ideaal voor unit testing, je hoeft geen echte server te draaien.

Veelvoorkomende valkuilen

Een paar fouten die ik regelmatig tegenkom in Go webapps:

  • Response al geschreven, als middleware al naar w schrijft, kan je handler niet meer naar willekeurige status terugvallen
  • next.ServeHTTP vergeten aan te roepen, je request blijft hangen zonder response
  • Geen return na http.Error, de handler draait alsnog door met een half geschreven response
  • Globale state in middleware, gebruik context of dependency injection in plaats van package variables
  • Te veel middleware op alle routes, niet elke route heeft auth of rate limiting nodig

Veelgestelde vragen

Wat is middleware in Go?

Middleware is een functie die zich tussen een inkomend HTTP request en je uiteindelijke handler bevindt. Het kan het request aanpassen, loggen, authenticeren of blokkeren voordat het de eindbestemming bereikt.

Heb ik een router library nodig in Go?

Voor kleine projecten is net/http met ServeMux vaak genoeg, zeker sinds Go 1.22 patterns met methodes en path variables ondersteunt. Voor complexere projecten bieden chi of gorilla/mux meer gemak met groeperen en middleware chains.

Wat is het verschil tussen chi en gorilla/mux?

Chi is lichtgewicht, idiomatisch en volledig compatibel met net/http. Gorilla/mux is volwassener en biedt meer matching opties, maar is sinds 2022 officieel gearchiveerd. Voor nieuwe projecten is chi meestal de betere keuze.

In welke volgorde moet ik middleware plaatsen?

Plaats recovery middleware als eerste zodat panics altijd worden opgevangen. Daarna komen logging, CORS, authenticatie en tot slot je route-specifieke middleware. De volgorde bepaalt hoe requests door de chain stromen.

Kan ik eigen middleware schrijven?

Ja, middleware in Go is simpelweg een functie die een http.Handler aanneemt en er een nieuwe teruggeeft. Hiermee kun je gemakkelijk logging, metrics of custom headers toevoegen zonder externe libraries.

Afsluitend

Middleware en routing geven je Go applicatie een schone structuur waarin cross-cutting concerns als logging, auth en recovery op één plek leven. Begin klein met net/http.ServeMux, stap over op chi als je meer nodig hebt, en bouw je middleware chain zorgvuldig op volgorde.

Wil je verder lezen? Kijk dan eens naar structs en interfaces om je handlers en services nog beter te ontwerpen, of database toegang in Go om je routes te koppelen aan persistente data.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.