Skip to content

Middleware

MeGaNeKo edited this page Mar 20, 2026 · 1 revision

Middleware

MiddlewareFunc Signature

All middleware uses the core.MiddlewareFunc type:

type MiddlewareFunc func(ctx Context, next func(Context))

Call next(ctx) to continue the chain. Skip it to short-circuit.

func logging(ctx core.Context, next func(core.Context)) {
    start := time.Now()
    next(ctx)
    fmt.Printf("%s %s %d %s\n",
        ctx.Method(), ctx.URL().Path, ctx.Status(), time.Since(start))
}

API-Level Middleware

Runs on every operation registered through the API:

api := neoma.NewAPI(config, adapter)
api.UseMiddleware(logging, auth)

Operation-Level Middleware

Attach middleware to a single operation via op.Middlewares:

neoma.Register(api, core.Operation{
    Method:      http.MethodPost,
    Path:        "/upload",
    Middlewares: core.Middlewares{rateLimitMiddleware},
}, handler)

Or with convenience functions:

neoma.Post[I, O](api, "/upload", handler, func(op *core.Operation) {
    op.Middlewares = core.Middlewares{rateLimitMiddleware}
})

Groups

Groups scope middleware, modifiers, and transformers to a subset of operations.

Creating Groups

import "github.com/MeGaNeKoS/neoma/middleware"

// Group with a path prefix
v1 := middleware.NewGroup(api, "/v1")
v1.UseMiddleware(v1Auth)

// Nested groups inherit parent middleware
admin := v1.Group("/admin")
admin.UseMiddleware(adminOnly)

// Only operations registered through this group get v1Auth + adminOnly
neoma.Get[I, O](admin, "/users", listAdminUsers)

UseModifier

Full control over whether an operation proceeds to registration:

grp.UseModifier(func(op *core.Operation, next func(*core.Operation)) {
    if op.Hidden {
        return // skip registration
    }
    next(op)
})

UseSimpleModifier

Modify the operation and always continue:

grp.UseSimpleModifier(func(op *core.Operation) {
    op.Tags = append(op.Tags, "v1")
})

UseTransformer

Scope response transformers to a group:

grp.UseTransformer(func(ctx core.Context, status string, v any) (any, error) {
    return map[string]any{"data": v}, nil
})

PrefixModifier

Register the same operation under multiple path prefixes:

grp := middleware.NewGroup(api)
grp.UseModifier(middleware.PrefixModifier([]string{"/v1", "/v2"}))

// Registers GET /v1/items AND GET /v2/items
neoma.Get[I, O](grp, "/items", handler)

middleware.Builder (Operation-Aware Middleware)

middleware.Builder creates middleware customized per-operation at registration time. This is useful when the middleware behavior depends on operation metadata.

type Builder interface {
    Build(op *core.Operation) core.MiddlewareFunc
}

middleware.BuilderFunc is a function adapter for Builder:

type BuilderFunc func(op *core.Operation) core.MiddlewareFunc

Example implementation:

type rateLimiter struct{ defaultRPS int }

func (r *rateLimiter) Build(op *core.Operation) core.MiddlewareFunc {
    rps := r.defaultRPS
    if v, ok := op.Metadata["rateLimit"]; ok {
        rps = v.(int)
    }
    return func(ctx core.Context, next func(core.Context)) {
        // rate limit at `rps` RPS
        next(ctx)
    }
}

middleware.NewBuilderModifier

Integrate a Builder with a group. It calls Build for each operation registered through the group and prepends the resulting middleware to op.Middlewares:

grp := middleware.NewGroup(api, "/api")
grp.UseModifier(middleware.NewBuilderModifier(&rateLimiter{defaultRPS: 100}))

neoma.Get[I, O](grp, "/expensive", handler, func(op *core.Operation) {
    op.Metadata = map[string]any{"rateLimit": 10}
})

middleware.Build

Build middleware for a specific operation directly:

mw := middleware.Build(&rateLimiter{defaultRPS: 100}, op)

Global Middleware

Global middleware is prepended to the middleware stack and runs before API-level middleware:

api.UseGlobalMiddleware(func(ctx core.Context, next func(core.Context)) {
    ctx.SetHeader("X-Powered-By", "Neoma")
    next(ctx)
})

Execution Order

  1. Global middleware (UseGlobalMiddleware)
  2. API-level middleware (UseMiddleware)
  3. Group middleware (outermost to innermost)
  4. Operation-level middleware (op.Middlewares)
  5. Handler

Each layer wraps the next; the first global middleware is the outermost function in the call chain.

Testing Middleware

The middleware package provides helpers for isolated testing.

TestMiddleware

Wrap a single middleware around a handler:

handler := middleware.TestMiddleware(myMiddleware, func(ctx core.Context) {
    ctx.SetStatus(200)
})
handler(testCtx)

TestChain

Test multiple middleware together:

chain := core.Middlewares{auth, logging}
handler := middleware.TestChain(chain, func(ctx core.Context) {
    ctx.SetStatus(200)
})
handler(testCtx)

TestBuilder

Test a Builder with a specific operation:

handler := middleware.TestBuilder(&rateLimiter{defaultRPS: 10}, op,
    func(ctx core.Context) {
        ctx.SetStatus(200)
    },
)
handler(testCtx)

Clone this wiki locally