Develop Custom Lua Plugins
Custom Lua plugins allow you to extend the functionality of API7 Gateway by executing custom logic during different phases of the request/response lifecycle. This guide walks you through creating a simple Lua plugin, registering it in API7 Gateway, and applying it to a route.
Prerequisites
- Basic knowledge of the Lua programming language.
- Familiarity with OpenResty and the Nginx request lifecycle.
- Access to an API7 Gateway instance and its Dashboard.
- A token from the Dashboard.
Plugin Structure
A Lua plugin is a module that exports a table containing its configuration schema, priority, and phase handlers.
Standard Fields
Every plugin should define the following fields:
name: The unique name of the plugin.version: The version of the plugin.priority: Determines the execution order. Plugins with higher priority run earlier.schema: A JSON schema that defines the configuration parameters for the plugin.
Execution Phases
API7 Gateway executes plugins in the following order:
rewrite: Modify the request before routing (e.g., changing the URI).access: Main processing phase (e.g., authentication, rate limiting).before_proxy: After the access phase, just before the request is proxied to the upstream.header_filter: Modify response headers.body_filter: Modify the response body.log: Record logs after the response has been sent to the client.
Step-by-Step Development
1. Create the Plugin File
Create a new Lua file for your plugin. For this example, we will create a plugin named custom-header.
2. Define the Schema and Priority
Use apisix.core to validate the plugin configuration against a JSON schema.
local core = require("apisix.core")
local schema = {
type = "object",
properties = {
header_name = {
type = "string",
default = "X-Custom-Lua"
},
header_value = {
type = "string",
default = "loaded"
}
}
}
local plugin_name = "custom-header"
local _M = {
version = 0.1,
priority = 1500,
name = plugin_name,
schema = schema,
}
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
3. Implement Phase Handlers
Implement the logic in the appropriate phase. To add a header to the request before it reaches the upstream, use the access phase.
function _M.access(conf, ctx)
core.request.set_header(ctx, conf.header_name, conf.header_value)
end
4. Complete Plugin Example
Here is the full code for the custom-header plugin:
local core = require("apisix.core")
local schema = {
type = "object",
properties = {
header_name = {
type = "string",
default = "X-Custom-Lua"
},
header_value = {
type = "string",
default = "loaded"
}
}
}
local plugin_name = "custom-header"
local _M = {
version = 0.1,
priority = 1500,
name = plugin_name,
schema = schema,
}
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
function _M.access(conf, ctx)
core.request.set_header(ctx, conf.header_name, conf.header_value)
end
return _M
Register the Plugin in API7 Gateway
In API7 Gateway, custom plugins are managed from the control plane. After you prepare the Lua source file, upload and deploy it from the Dashboard so it becomes available in the plugin picker for routes, services, consumers, or global rules.
1. Add the Plugin in the Dashboard
In the Dashboard, go to Gateway Settings and open the Custom Plugin tab, then click Add Custom Plugin.
Upload the Lua source file and provide the required plugin metadata:
- Plugin Source Code File: upload
custom-header.lua - Plugin Description: briefly describe what the plugin does
- Plugin Documentation Link: optionally link to your plugin documentation
- Plugin Author: identify the owner of the plugin
- Deployed Gateway Group: select the gateway groups where the plugin should be available
When the file is uploaded successfully, API7 Gateway parses the plugin and displays its detected name and version in the dialog before you add it.
After the plugin is added, it can be selected from the Add Plugin dialog on routes and services in the deployed gateway groups.
For the product workflow, see Custom Plugins.
2. Confirm the Plugin is Available
Open a route or service in one of the deployed gateway groups and confirm that custom-header appears in the Add Plugin dialog.
Add Dependency Libraries
A custom plugin you upload from the Dashboard is registered as a single, self-contained plugin module. The control plane distributes its source code to the gateways and loads it directly into the runtime. The file is not written to the gateway's Lua module search path. Because of this, an uploaded custom plugin is not reliably resolvable through require() from another plugin. You should not upload a shared library as a custom plugin for other plugins to require. That may appear to work at first, but it breaks after a gateway restart: the dependency is not guaranteed to load before the plugins that require it.
Some plugins share helper code, or depend on a third-party Lua module or a native (C) library. Install that dependency on the gateway's package path, and require it from your plugin as usual.
Place the Dependency on the Lua or C Path
The gateway resolves modules through two search paths:
- Lua modules are resolved through
lua_package_path. A file at<dir>/myorg/utils.luais loaded withrequire("myorg.utils"). - Native Lua/C modules are resolved through
lua_package_cpath. A file at<dir>/myorg/native.sois loaded withrequire("myorg.native").
You can use either the gateway's built-in paths or your own directories:
-
Built-in path — place the file under the gateway's built-in search path (for example,
/usr/local/apisix/), matching the module name yourequire. -
Extra path (recommended) — keep your code separate from the runtime and add your own directory in the Data Plane
config.yaml:apisix:
extra_lua_path: "/data/lua/?.lua" # extends lua_package_path
extra_lua_cpath: "/data/clib/?.so" # extends lua_package_cpathFor details, see Custom Plugin Paths and the configuration reference.
A native library loaded through LuaJIT FFI with ffi.load() is resolved by the system dynamic linker, not by lua_package_cpath. Place such libraries on the system library path (for example, /usr/local/lib) or set LD_LIBRARY_PATH on the gateway.
Make the Files Available to the Gateway
The dependency files must exist on every gateway node before the gateway starts:
- Kubernetes / Helm — mount the files into the gateway pod with a volume (such as a ConfigMap or a persistent volume) and point
extra_lua_path/extra_lua_cpathat the mount path. - Docker / Docker Compose — bind-mount the directory into the gateway container.
Unlike uploaded custom plugins, which are distributed dynamically by the control plane, libraries on the package path are loaded when the gateway worker starts. After you add or change them, reload or restart the gateway for the change to take effect.
Example
Put shared helpers in a plain Lua module on the gateway's path — for example, /data/lua/myorg/base.lua:
local _M = {}
function _M.verify(token)
-- shared logic used by multiple plugins
return token ~= nil
end
return _M
Add the directory to the Data Plane configuration:
apisix:
extra_lua_path: "/data/lua/?.lua"
Then require it from any custom plugin:
local core = require("apisix.core")
local base = require("myorg.base")
-- ...
function _M.access(conf, ctx)
if not base.verify(core.request.header(ctx, "Authorization")) then
return 401
end
end
Because the module is on the package path, require("myorg.base") resolves identically on every worker and survives gateway restarts.
Test the Plugin
Once the plugin source code is available on the gateway node and the plugin is registered in API7 Gateway, you can enable it on a specific route using the Admin API or ADC.
- Create a route with the plugin enabled:
- Admin API
- ADC
curl -k "https://localhost:7443/apisix/admin/services/custom-header-service?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "custom-header-service",
"upstream": {
"type": "roundrobin",
"scheme": "http",
"nodes": [
{
"host": "httpbin.org",
"port": 80,
"weight": 100
}
]
}
}'
curl -k "https://localhost:7443/apisix/admin/routes/custom-header-route?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "custom-header-route",
"methods": ["GET"],
"paths": ["/get"],
"service_id": "custom-header-service",
"plugins": {
"custom-header": {
"header_name": "X-Trace-ID",
"header_value": "docs-test"
}
}
}'
services:
- name: custom-header-service
upstream:
nodes:
- host: httpbin.org
port: 80
weight: 1
routes:
- name: custom-header-route
uris:
- /get
plugins:
custom-header:
header_name: X-Trace-ID
header_value: docs-test
adc sync -f adc.yaml
- Send a request and verify the header on the upstream response body:
curl -i "http://localhost:9080/get"
The httpbin response should include X-Trace-ID: docs-test under headers, which confirms the plugin modified the request before proxying.
If the proxy returns 404 Route Not Found immediately after the route is created, wait a few seconds and retry the request.
Debugging Tips
- Check Error Logs: By default, logs are located at
/usr/local/apisix/logs/error.log. - Syntax Errors: Use
luac -p custom-header.luato check for Lua syntax errors before uploading. - Plugin Visibility: If the plugin does not appear in the Dashboard plugin list, verify it was deployed to the correct gateway group.
- Admin API Payload Shape: In API7 Gateway, route creation uses
pathsandservice_idrather than APISIX-style top-leveluriand inlineupstream. - Name Conflicts: Ensure the plugin name is unique and does not collide with a built-in plugin.
Next Steps
- Plugins — understand plugin execution order and scopes.