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.
Try it on Codelit
Chaos Mode
Simulate node failures and watch cascading impact across your architecture
Cost Estimator
See estimated AWS monthly costs for every component in your architecture
Related articles
API Backward Compatibility: Ship Changes Without Breaking Consumers
6 min read
api designBatch API Endpoints — Patterns for Bulk Operations, Partial Success, and Idempotency
8 min read
system designCircuit Breaker Implementation — State Machine, Failure Counting, Fallbacks, and Resilience4j
7 min read
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 componentsBuild this architecture
Generate an interactive architecture for API Gateway Custom Plugins in seconds.
Try it in Codelit →
Comments