Most API gateways are extensible in theory. In practice, you end up reading source code for hours before writing a single line of business logic.
I ran into this while building Kono. Before deciding on the plugin architecture, I looked at how KrakenD handles request/response modification. Their approach is powerful but has real entry cost: you implement a ModifierRegisterer symbol, call RegisterModifiers with factory functions, copy-paste RequestWrapper and ResponseWrapper interface definitions into your plugin (they use interface{} to avoid dependency collisions), and wire everything through extra_config in JSON. The minimal boilerplate is around 60 lines before you write any logic.
Here's what that looks like just to register a modifier in KrakenD:
var ModifierRegisterer = registerer("my-plugin")
func (r registerer) RegisterModifiers(f func(
name string,
factoryFunc func(map[string]interface{}) func(interface{}) (interface{}, error),
appliesToRequest bool,
appliesToResponse bool,
)) {
f(string(r)+"-request", r.requestHandler, true, false)
}
Then your actual modifier receives an interface{} that you type-assert against a RequestWrapper you defined yourself. If the assertion fails, you return an error. It works, but it's indirection on top of indirection.
What Kono does instead
A Kono plugin is a Go file that implements four methods: Info(), Type(), Init(), and Execute(). The Context gives you direct access to http.Request and http.Response — no wrapper interfaces to copy, no registerers, no factory functions.
func (p *Plugin) Execute(ctx sdk.Context) error {
resp := ctx.Response()
if resp == nil || resp.Body == nil {
return nil
}
// read body, transform, replace
resp.Body = io.NopCloser(bytes.NewReader(newBody))
return nil
}
The entire SDK surface is: constants for plugin type, a PluginInfo struct, and a Context. That's it. The snakeify plugin — which recursively transforms JSON keys to snake_case — is about 100 lines total. Roughly 30 are SDK, the rest is logic.
Middleware follows standard Go http.Handler wrapping — if you've written middleware before, there's nothing new:
func (m *Middleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// before request
next.ServeHTTP(w, r)
// after response
})
}
Lua without the binding overhead
For cases where recompiling a plugin is too heavy - token validation, header injection, path rewriting - Kono has Lumos, a Lua scripting layer.
The common approach is embedding a Lua interpreter with Go bindings. KrakenD does this. It works, but binding overhead accumulates under load, and you're limited by what the binding exposes.
Lumos takes a different route: a separate LuaJIT process communicating with the gateway over a Unix socket using a length-prefixed JSON protocol. LuaJIT is significantly faster than interpreted Lua with Go bindings, and since Lumos runs in the same container as the gateway, there's no network hop - Unix socket latency is in microseconds.
A script receives a JSON payload with the full request and returns either continue with optional modifications or abort with an HTTP status code. No Lua SDK to install, no bindings to configure.
Give it a try
Kono is built for teams who want a gateway they can extend without reading its internals first - small surface, clear contracts, no magic. If that sounds like your setup, give it a try.
- GitHub: github.com/starwalkn/kono
- Docs: starwalkn.github.io/konodocs
Top comments (0)