AI-Assisted NGINX Scripting with OpenResty and Lua
Extend NGINX with OpenResty and Lua using AI as a drafting aid: when Lua beats plain config, the request phases, non-blocking cosockets, and keeping modules reviewable.
- #nginx
- #ai
- #openresty
- #lua
The first time I reached for OpenResty, I’d spent an afternoon trying to express a piece of dynamic routing logic in plain NGINX config and lost. Some logic genuinely doesn’t fit the declarative model — you need to call a backend to make a decision, transform a payload, or compute a key from several inputs at request time. OpenResty embeds LuaJIT into NGINX and lets you do exactly that, and it’s powerful enough to be dangerous. AI is a good drafting partner for Lua because it knows the OpenResty API, but Lua in the request path is code you own forever, so I keep modules small and I verify behavior rather than trusting the draft.
This guide is about using OpenResty deliberately: AI drafts the Lua, you decide whether you needed it and confirm it behaves.
First question: do you even need Lua?
The most valuable habit with OpenResty is asking whether a map, a return, or an auth_request would do the job first. Lua is maintenance you take on, and a surprising amount of “I need scripting” turns out to be a map block in disguise. So before any Lua, I ask the assistant to talk me out of it:
I want to do [DESCRIBE LOGIC] in NGINX. Before writing any Lua, tell me whether plain config — map, geo, return, auth_request, or try_files — can do this. Only if it genuinely cannot, draft minimal OpenResty Lua.
About a third of the time the answer is “use a map,” and I’ve saved myself a module. That framing alone makes the AI more useful, because it pushes back instead of eagerly writing code.
The phases that make Lua make sense
OpenResty hooks Lua into NGINX’s request lifecycle at specific phases, and which directive you use determines what you can do. The ones you’ll actually use:
access_by_lua_block— runs before the request is served; the place for auth, rate decisions, or rejecting requests.content_by_lua_block— generates the response itself; turns NGINX into a tiny app server for an endpoint.rewrite_by_lua_block— manipulates the URI/args before routing.header_filter_by_lua_block/body_filter_by_lua_block— rewrite the response on the way out.
Getting the phase wrong is the most common OpenResty bug: people try to read the request body in a phase where it isn’t available yet, or set a response header after it’s already been sent. When AI drafts Lua, I make it name the phase and explain why, because that’s where the subtle failures live.
A minimal, reviewable example
Say I want to reject requests whose X-Api-Version header is below a minimum, returning a clean JSON error. That’s borderline — a map plus a return could nearly do it, but the JSON body and the comparison push it into Lua territory. Here’s the prompt:
Draft a minimal OpenResty access_by_lua_block that reads the X-Api-Version request header, and if it’s missing or less than 2, returns a 400 with a JSON body {“error”:“unsupported api version”}. Handle the missing-header case safely. Comment the phase. Output config only.
The result, after review:
location /api/ {
access_by_lua_block {
-- access phase: runs before the request reaches the backend
local ver = tonumber(ngx.req.get_headers()["X-Api-Version"])
if not ver or ver < 2 then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.header.content_type = "application/json"
ngx.say('{"error":"unsupported api version"}')
return ngx.exit(ngx.HTTP_BAD_REQUEST)
end
-- otherwise fall through to proxy_pass below
}
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
What I check in any generated Lua like this:
- The missing-input guard.
tonumber(nil)returnsnil, and thenot vercheck handles the absent header. An AI draft that assumes the header is always present will throw a Lua error — which becomes a 500 for a real user. ngx.exitafterngx.say, so the request actually terminates instead of continuing to the backend.- Module size. If the block grows past ~40 lines, I move it to a
.luafile loaded withcontent_by_lua_fileso it’s testable and reviewable, not buried in the config.
The non-blocking rule
The single most important thing to understand about OpenResty: everything in the request path must be non-blocking. LuaJIT runs inside the NGINX worker, so a blocking call — a synchronous socket, a os.execute, a sleep — stalls every other request that worker is handling. OpenResty provides non-blocking equivalents (cosockets, ngx.timer, the lua-resty-* libraries), and you must use them.
When I ask AI to draft anything that talks to a database or another service from Lua, I’m explicit:
This runs in the NGINX request path. Use only non-blocking OpenResty cosocket APIs (e.g. lua-resty-redis), never a blocking socket library. Show the connection-pool setkeepalive call.
A blocking Redis call in access_by_lua will work perfectly in a one-request test and fall over under load, which is the worst kind of bug — invisible until traffic arrives.
Validate before you reload
OpenResty configs go through the same gate, and nginx -t does catch Lua syntax errors at load time, which is genuinely helpful:
sudo nginx -t
# Catches Lua syntax errors too, not just NGINX directive errors
sudo nginx -s reload
But nginx -t cannot catch a logic bug, an unhandled nil, or a blocking call. So I test the branches that matter — including the malformed input the AI’s happy-path draft probably ignored:
# Valid version: should reach the backend
curl -i https://example.com/api/users -H "X-Api-Version: 3"
# Missing/old version: should get the JSON 400, not a 500
curl -i https://example.com/api/users
curl -i https://example.com/api/users -H "X-Api-Version: 1"
If the missing-header case returns a 500 instead of the clean 400, the Lua threw — go back and fix the guard.
Where AI fits
AI drafted correct OpenResty idioms, knew the phase APIs, and reminded me about setkeepalive for connection pooling when I asked. What it didn’t do unprompted was talk me out of Lua when a map would have worked, or guard the malformed-input path that turns a clean rejection into a 500. Those are the calls that keep a small Lua module from becoming a production liability. Draft with AI, validate with nginx -t, and test the unhappy paths yourself.
For more, the AI for NGINX category collects related patterns, the njs scripting prompt is worth comparing if you want a lighter-weight scripting option than full OpenResty, and the prompt library has the map-based alternatives that often replace Lua entirely.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.