API Gateway Custom Plugins — Extending Kong, Envoy, and NGINX
Why build custom gateway plugins?#
API gateways like Kong, Envoy, and NGINX come with built-in plugins for authentication, rate limiting, and logging. But production systems inevitably need custom logic: tenant-aware routing, proprietary auth schemes, request enrichment from internal services, or compliance transformations.
Custom plugins let you run this logic at the gateway layer — before requests reach your backend — reducing latency and centralizing cross-cutting concerns.
Kong plugin development#
Kong plugins are written in Lua and run inside the OpenResty runtime (NGINX + LuaJIT). Each plugin can hook into multiple phases of the request lifecycle.
Kong request lifecycle phases#
- certificate — TLS handshake (for custom certificate selection)
- rewrite — modify the request before routing
- access — authentication, authorization, rate limiting
- header_filter — modify response headers
- body_filter — modify response body
- log — async logging after response is sent
Basic Kong plugin structure#
-- kong/plugins/my-auth/handler.lua
local MyAuth = {
PRIORITY = 1000,
VERSION = "1.0.0",
}
function MyAuth:access(conf)
local token = kong.request.get_header("X-API-Key")
if not token then
return kong.response.exit(401, {
message = "Missing API key"
})
end
-- Validate against your auth service
local httpc = require("resty.http").new()
local res, err = httpc:request_uri(conf.auth_url, {
method = "POST",
body = kong.service.request.encode({ token = token }),
headers = { ["Content-Type"] = "application/json" },
})
if not res or res.status ~= 200 then
return kong.response.exit(403, {
message = "Invalid API key"
})
end
-- Pass user info to upstream
local user = require("cjson").decode(res.body)
kong.service.request.set_header("X-User-ID", user.id)
kong.service.request.set_header("X-User-Tier", user.tier)
end
return MyAuth
Kong plugin schema#
Every plugin needs a schema file that defines its configuration:
-- kong/plugins/my-auth/schema.lua
local typedefs = require "kong.db.schema.typedefs"
return {
name = "my-auth",
fields = {
{ consumer = typedefs.no_consumer },
{ protocols = typedefs.protocols_http },
{ config = {
type = "record",
fields = {
{ auth_url = { type = "string", required = true } },
{ cache_ttl = { type = "number", default = 300 } },
},
},
},
},
}
Testing Kong plugins#
Kong provides a test framework based on busted:
local helpers = require "spec.helpers"
describe("my-auth plugin", function()
local proxy_client
setup(function()
local bp = helpers.get_db_utils()
local route = bp.routes:insert({ paths = { "/test" } })
bp.plugins:insert({
name = "my-auth",
route = { id = route.id },
config = { auth_url = "http://auth-service/validate" },
})
helpers.start_kong()
proxy_client = helpers.proxy_client()
end)
it("rejects requests without API key", function()
local res = proxy_client:get("/test")
assert.response(res).has.status(401)
end)
end)
Envoy filters#
Envoy supports custom filters through WebAssembly (WASM) and native C++ extensions. WASM is the preferred approach for most teams because it avoids recompiling Envoy.
Envoy WASM filter in Rust#
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
proxy_wasm::main! {{
proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> {
Box::new(MyFilter)
});
}}
struct MyFilter;
impl HttpContext for MyFilter {
fn on_http_request_headers(
&mut self,
_num_headers: usize,
_end_of_stream: bool
) -> Action {
// Check for required header
match self.get_http_request_header("x-api-key") {
Some(key) if !key.is_empty() => {
// Add tenant context
self.set_http_request_header(
"x-tenant-id",
Some(&self.extract_tenant(&key))
);
Action::Continue
}
_ => {
self.send_http_response(
401,
vec![],
Some(b"Missing API key"),
);
Action::Pause
}
}
}
}
Envoy filter chain configuration#
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/etc/envoy/filters/my_filter.wasm"
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
Envoy filter advantages#
- Language flexibility — write in Rust, Go, C++, or AssemblyScript
- Sandboxed execution — WASM filters cannot crash the proxy
- Hot reloading — deploy new filter versions without restarting Envoy
- Portable — the same WASM binary runs on any Envoy-based gateway (Istio, Gloo, etc.)
NGINX Lua modules#
NGINX with OpenResty supports Lua scripting at various request phases, similar to Kong but without the plugin framework overhead.
Request transformation with Lua#
-- /etc/nginx/lua/transform_request.lua
local cjson = require "cjson"
-- Read request body
ngx.req.read_body()
local body = ngx.req.get_body_data()
if body then
local data = cjson.decode(body)
-- Add server-side fields
data.processed_at = ngx.now()
data.source_ip = ngx.var.remote_addr
-- Remove sensitive fields before forwarding
data.internal_token = nil
local transformed = cjson.encode(data)
ngx.req.set_body_data(transformed)
ngx.req.set_header("Content-Length", #transformed)
end
NGINX configuration#
location /api/ {
access_by_lua_file /etc/nginx/lua/auth_check.lua;
rewrite_by_lua_file /etc/nginx/lua/transform_request.lua;
header_filter_by_lua_file /etc/nginx/lua/add_headers.lua;
proxy_pass http://backend;
}
Common plugin patterns#
Custom authentication#
All three gateways support custom auth plugins. The typical flow:
- Extract credentials from the request (header, cookie, query param)
- Validate against an auth service or JWT verification
- Inject user identity headers for the upstream service
- Cache validation results to reduce auth service load
Request transformation#
Transform requests before they reach your backend:
- Field injection — add timestamps, trace IDs, or tenant context
- Field removal — strip internal headers or sensitive data
- Format conversion — XML to JSON, query params to body
- Schema validation — reject malformed payloads at the gateway
Rate limiting plugins#
Build on top of the built-in rate limiters when you need custom logic:
- Tier-based limits — different thresholds per user plan
- Endpoint cost — weight expensive endpoints higher
- Adaptive limits — tighten limits when backend latency increases
- Quota pooling — share rate limits across related API keys
Plugin development best practices#
- Keep plugins stateless — store state in Redis or an external service, not in plugin memory
- Fail gracefully — if your auth service is down, decide whether to fail open or closed
- Minimize latency — every millisecond in the plugin adds to every request
- Log and monitor — emit metrics for plugin execution time and error rates
- Version your plugins — use semantic versioning and test backward compatibility
- Cache aggressively — cache auth decisions, rate limit counters, and config lookups
Choosing the right gateway for plugins#
| Feature | Kong | Envoy | NGINX + OpenResty |
|---|---|---|---|
| Plugin language | Lua | WASM (Rust, Go, C++) | Lua |
| Plugin ecosystem | Large (Kong Hub) | Growing | Medium |
| Hot reload | Yes (DB-backed) | Yes (xDS API) | Partial (HUP signal) |
| Admin API | Built-in REST API | xDS / control plane | Third-party |
| Best for | API management | Service mesh | High-performance proxy |
Visualize gateway plugin architecture#
On Codelit, generate an API gateway architecture with custom plugin chains to see how requests flow through authentication, transformation, and rate limiting stages before reaching your services.
This is article #409 in the Codelit engineering blog series.
Build and explore API gateway architectures visually at codelit.io.
Explore the WhatsApp architecture interactively
Try it →Try these templates
Multiplayer Game Backend
Real-time multiplayer game server with matchmaking, state sync, leaderboards, and anti-cheat.
8 componentsCustomer Support Platform
Zendesk-like helpdesk with tickets, live chat, knowledge base, and AI-powered auto-responses.
8 componentsAPI Gateway Platform
Kong/AWS API Gateway-like platform with routing, auth, rate limiting, transformation, and developer portal.
8 componentsContinue learning
Go deeper on API design
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
API Design Patterns
JJ Geewax · 2021
Chapter-length treatment of pagination, long-running operations and versioning, with trade-offs.
Docs for Developers
Jared Bhatti, Zachary Sarah Corleissen, Jen Lambourne, David Nunez, Heidi Waterhouse · 2021
Turns API docs into a repeatable process: audience research, drafting, review, measuring use.
4.4 (156)Kindle editionContract Testing in Action
Marie Cruz, Lewis Prescott · 2024
Stands up Pact contract tests between consumer and provider and wires them into a CI pipeline.
4.3 (6)Kindle editionLearning Domain-Driven Design
Vlad Khononov · 2021
Turns bounded contexts and aggregates into decision rules for where service boundaries actually go.
4.6 (400)