Write Custom Middleware
Write Custom Middleware
Section titled “Write Custom Middleware”This guide shows the canonical pattern for writing a Plumego middleware: a constructor function that accepts dependencies explicitly and returns a middleware.Middleware value.
For the boundary rationale, see the Middleware Primer.
What this guide covers
Section titled “What this guide covers”- The
middleware.Middlewaretype signature - Writing a constructor that accepts a logger and returns a middleware
- Registering it with
app.Use - The recoverable variant pattern with
*Econstructors
The middleware type
Section titled “The middleware type”All Plumego middleware satisfies one type:
type Middleware func(http.Handler) http.HandlerYour constructor returns this type. The inner http.HandlerFunc is where your logic runs.
Step 1 — Write the constructor
Section titled “Step 1 — Write the constructor”Follow the same shape as the built-in middleware: a public constructor that panics on bad deps, and a *E variant that returns an error.
package requesttimer
import ( "net/http" "time"
"github.com/spcent/plumego/log" "github.com/spcent/plumego/middleware")
// Middleware logs the elapsed time for every request.func Middleware(logger log.StructuredLogger) middleware.Middleware { mw, err := MiddlewareE(logger) if err != nil { panic(err.Error()) } return mw}
// MiddlewareE is the error-returning variant for callers that prefer not to panic.func MiddlewareE(logger log.StructuredLogger) (middleware.Middleware, error) { if logger == nil { return nil, errors.New("requesttimer: logger cannot be nil") } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) elapsed := time.Since(start) logger.Info("request timing", log.Fields{ "method": r.Method, "path": r.URL.Path, "elapsed": elapsed.String(), }) }) }, nil}Step 2 — Register with app.Use
Section titled “Step 2 — Register with app.Use”import "myapp/internal/middleware/requesttimer"
app.Use(requesttimer.Middleware(app.Logger()))app.Use applies middleware in registration order. Middleware added first runs outermost — it wraps all later middleware and the final handler.
Step 3 — Ordering matters
Section titled “Step 3 — Ordering matters”A common ordering for a service that uses the built-in middleware:
recoveryMw, err := recovery.Middleware(recovery.Config{Logger: app.Logger()})if err != nil { return err}accesslogMw, err := accesslog.Middleware(accesslog.Config{Logger: app.Logger()})if err != nil { return err}app.Use(requestid.Middleware()) // attach request ID firstapp.Use(recoveryMw) // catch panics before loggingapp.Use(accesslogMw) // log after ID existsapp.Use(requesttimer.Middleware(app.Logger())) // your custom middleware lastRules for keeping middleware transport-only
Section titled “Rules for keeping middleware transport-only”- Read and write HTTP headers, status codes, and the response body.
- Do not make business decisions or call domain services directly.
- Accept all dependencies through the constructor — no package-level globals.
- Pass unknown errors through; do not swallow panics unless you are a recovery middleware.
What this pattern gives you
Section titled “What this pattern gives you”- Every dependency is visible at the call site; no implicit globals.
- The
*Evariant lets tests verify constructor errors without recovering panics. - Middleware stays composable: any function returning
middleware.Middlewareworks withapp.Use.
If this does not work
Section titled “If this does not work”| Symptom | Check first |
|---|---|
| Middleware never runs | Register it with app.Use before Prepare, or wrap the route handler directly |
| Handler stops unexpectedly | Confirm your middleware calls next.ServeHTTP(w, r) exactly when the request should continue |
| Ordering is wrong | Remember first registered middleware runs outermost |
| Constructor panics in tests | Use the *E variant to assert dependency validation |
| Middleware starts calling services | Move business decisions back to handlers or application code; middleware stays transport-only |