News KrakenD 3.0 Is Here: AI Router, Semantic Cache, and On-the-Fly Stream Manipulation News KrakenD CE 3.0.1, EE 3.0.1 and EE 2.13.11 Update Released News KrakenD Partners with CGK Solutions to Secure API Integration in Italy Case Study Every Change Reviewed, Every Peak Absorbed: Paribu's API Gateway

Document updated on Oct 1, 2026

Middleware Plugins for KrakenD API Gateway

Middleware Plugins for KrakenD API Gateway

Middleware plugins are designed to provide the maximum level of customization. They let you change the internals of KrakenD the same way forking the source code and adding your own middleware would, but without maintaining a fork or your own builds.

Middleware plugins allow you to add functionality to the remaining parts, whereas the other plugins do not allow you to do so. The primary purpose of Middleware plugins is:

  • Intercept the request and response
  • Delegate the execution to custom code
  • Abort the execution (e.g., you implemented a custom usage policy)
  • Transform data at your will

Proxy interface

When you add a Middleware plugin, it receives a context and a request, works with it, and passes the execution to the next proxy component. It can live both in the endpoint or the backend.

The Middleware executes before the request and response modifier plugins (if any), before instrumentation, and after the rest of enterprise and FOSS functionality in that pipe zone. When placed in a backend, it executes between the rate limit and JMESpath executions, and when placed at the endpoint level, it executes between the Workflows and the Response Json Schema

Example: Building your first middleware plugin

To build a middleware plugin execute the following command:

Generate a middleware plugin 

$docker run --rm -it \
 -w /app \
 -e "KRAKEND_LICENSE_PATH=/LICENSE"\
 -v "$PWD/LICENSE:/LICENSE:ro"
 -v "$PWD/plugins:/app" \
 krakend/krakend-ee:3.0.1 plugin init --name my-mw --type middleware

See the following main.go that modifies the data adding an acme field and modifying the path.

package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/url"
)

// MiddlewareRegisterer is the symbol the plugin loader will be looking for. It must
// implement the MiddlewareRegisterer interface
//   type MiddlewareRegisterer interface {
//     RegisterMiddlewares(func(
//       string,
//       func(map[string]interface{}, func(context.Context, interface{}) (interface{}, error)) func(context.Context, interface{}) (interface{}, error),
//     ))
//   }
var MiddlewareRegisterer = registerer("mw-example")

var (
	logger                Logger          = nil
	ctx                   context.Context = context.Background()
	errUnkownRequestType                  = errors.New("unknown request type")
	errUnkownResponseType                 = errors.New("unknown response type")
)

type registerer string

// RegisterMiddlewares is the function the plugin loader will call to register the
// middleware(s) contained in the plugin using the function passed as argument.
// f will register the factoryFunc under the name.
func (r registerer) RegisterMiddlewares(f func(
	name string,
	middlewareFactory func(map[string]interface{}, func(context.Context, interface{}) (interface{}, error)) func(context.Context, interface{}) (interface{}, error),
),
) {
	f(string(r), r.middlewareFactory)
}

func (r registerer) middlewareFactory(cfg map[string]interface{}, next func(context.Context, interface{}) (interface{}, error)) func(context.Context, interface{}) (interface{}, error) {
	// If the middleware requires configuration, declare it under its "config" key.
	// ex: the following example adds a parameter "option" that the plugin can use.
	/*
	   "extra_config":{
	       "plugin/middleware":{
	           "plugins":[
	               {
	                   "name":"mw-example",
	                   "config":{
	                       "option": "value"
	                   }
	               }
	           ]
	       }
	   }

	// Uncomment if you have configuration:
	config, err := r.parseConfig(cfg)
	if err != nil {
		logger.Debug(fmt.Sprintf("[PLUGIN: %s] Cannot initialize configuration: %s", r, err))
		return nil
	}
	*/

	// This is an example of registered ctx usage, it can be used to, for example, connect to external services.
	select {
	case <-ctx.Done():
		logger.Debug(fmt.Sprintf("[PLUGIN: %s] Context is done, skipping middleware injection", r))
		return nil
	default:
	}

	logger.Debug(fmt.Sprintf("[PLUGIN: %s] Middleware injected", r))
	return func(ctx context.Context, req interface{}) (interface{}, error) {
		reqw, ok := req.(RequestWrapper)
		if !ok {
			return nil, errUnkownRequestType
		}

		resp, err := next(ctx, requestWrapper{
			params:  reqw.Params(),
			headers: reqw.Headers(),
			body:    reqw.Body(),
			method:  reqw.Method(),
			url:     reqw.URL(),
			query:   reqw.Query(),
			// Modifies the path:
			path:    reqw.Path() + "/fooo",
		})
		respw, ok := resp.(ResponseWrapper)
		if !ok {
			return nil, errUnkownResponseType
		}

		// Add an "acme" field in the response.
		data := respw.Data()
		data["acme"] = true

		return responseWrapper{
			ctx:        respw.Context(),
			request:    respw.Request(),
			data:       data,
			isComplete: respw.IsComplete(),
			metadata: metadataWrapper{
				headers:    respw.Headers(),
				statusCode: respw.StatusCode(),
			},
			io: respw.Io(),
		}, err
	}
}

type config struct {
	Opt string `json:"option"`
}

// parseConfig parses the configuration marshaling and unmarshaling into a struct.
// The received map is the content of "config", so you can also check its fields manually
func (r registerer) parseConfig(extra map[string]interface{}) (*config, error) {
	cfg := config{}
	b, err := json.Marshal(extra)
	if err != nil {
		return nil, err
	}
	if err = json.Unmarshal(b, &cfg); err != nil {
		return nil, err
	}
	return &cfg, nil
}

// This logger is replaced by the RegisterLogger method to load the one from KrakenD
func (r registerer) RegisterLogger(in interface{}) {
	l, ok := in.(Logger)
	if !ok {
		return
	}
	logger = l
	logger.Debug(fmt.Sprintf("[PLUGIN: %s] Logger loaded", r))
}

type Logger interface {
	Debug(v ...interface{})
	Info(v ...interface{})
	Warning(v ...interface{})
	Error(v ...interface{})
	Critical(v ...interface{})
	Fatal(v ...interface{})
}

// Empty logger implementation
type noopLogger struct{}

func (n noopLogger) Debug(_ ...interface{})    {}
func (n noopLogger) Info(_ ...interface{})     {}
func (n noopLogger) Warning(_ ...interface{})  {}
func (n noopLogger) Error(_ ...interface{})    {}
func (n noopLogger) Critical(_ ...interface{}) {}
func (n noopLogger) Fatal(_ ...interface{})    {}

// RegisterContext saves the KrakenD application context so KrakenD can inject
// the context like we do with the logger
func (r registerer) RegisterContext(c context.Context) {
	ctx = c
	logger.Debug(fmt.Sprintf("[PLUGIN: %s] Context loaded", r))
}

func main() {}

// RequestWrapper is an interface for passing proxy request between the krakend pipe
// and the loaded plugins
type RequestWrapper interface {
	Context() context.Context
	Params() map[string]string
	Headers() map[string][]string
	Body() io.ReadCloser
	Method() string
	URL() *url.URL
	Query() url.Values
	Path() string
}

type requestWrapper struct {
	ctx     context.Context
	method  string
	url     *url.URL
	query   url.Values
	path    string
	body    io.ReadCloser
	params  map[string]string
	headers map[string][]string
}

func (r requestWrapper) Context() context.Context     { return r.ctx }
func (r requestWrapper) Method() string               { return r.method }
func (r requestWrapper) URL() *url.URL                { return r.url }
func (r requestWrapper) Query() url.Values            { return r.query }
func (r requestWrapper) Path() string                 { return r.path }
func (r requestWrapper) Body() io.ReadCloser          { return r.body }
func (r requestWrapper) Params() map[string]string    { return r.params }
func (r requestWrapper) Headers() map[string][]string { return r.headers }

type metadataWrapper struct {
	headers    map[string][]string
	statusCode int
}

func (m metadataWrapper) Headers() map[string][]string { return m.headers }
func (m metadataWrapper) StatusCode() int              { return m.statusCode }

// ResponseWrapper is an interface for passing proxy response between the krakend pipe
// and the loaded plugins
type ResponseWrapper interface {
	Context() context.Context
	Request() interface{}
	Data() map[string]interface{}
	IsComplete() bool
	Headers() map[string][]string
	StatusCode() int
	Io() io.Reader
}

type responseWrapper struct {
	ctx        context.Context
	request    interface{}
	data       map[string]interface{}
	isComplete bool
	metadata   metadataWrapper
	io         io.Reader
}

func (r responseWrapper) Context() context.Context     { return r.ctx }
func (r responseWrapper) Request() interface{}         { return r.request }
func (r responseWrapper) Data() map[string]interface{} { return r.data }
func (r responseWrapper) IsComplete() bool             { return r.isComplete }
func (r responseWrapper) Io() io.Reader                { return r.io }
func (r responseWrapper) Headers() map[string][]string { return r.metadata.headers }
func (r responseWrapper) StatusCode() int              { return r.metadata.statusCode }

Once compiled, inject the middleware plugin into your configuration. The following configuration loads the mw-example plugin above in an endpoint and passes it the option setting:

{
  "version": 4,
  "extra_config": {
    "plugin": {
      "pattern": ".so",
      "folder": "/opt/krakend/plugins/"
    }
  },
  "endpoints": [
    {
      "endpoint": "/foo",
      "extra_config": {
        "plugin/middleware": {
          "plugins": [
            {
              "name": "mw-example",
              "config": {
                "option": "value"
              }
            }
          ]
        }
      },
      "backend": [
        {
          "host": ["http://localhost:8080"],
          "url_pattern": "/__debug/"
        }
      ]
    }
  ]
}

KrakenD passes the content of config to the middlewareFactory function as its cfg map. The plugin then appends /fooo to the path it sends to the backend and adds "acme": true to the response.

Unresolved issues?

The documentation is only a piece of the help you can get! Whether you are looking for Open Source or Enterprise support, see more support channels that can help you.

See all support channels