Nginx OpenResty API Gateway with Rate Limiting and Auth
Build an API gateway with OpenResty, a version of Nginx that runs Lua code inside the web server. Every request passes through your Lua code before reaching the backend services: it checks the JWT token (signature, expiry and scopes) with lua-resty-jwt, enforces a request limit per client using memory shared between all Nginx workers, and then forwards the request to the right backend. Because the code runs inside Nginx, the gateway adds almost no delay, even under heavy load. You'll learn the Nginx request phases, Lua in OpenResty, JWT validation, rate limiting algorithms with shared dictionaries, proxying and upstreams, and load testing and tuning.
- Estimate
- ~14h
- Steps
- 7
- Completed by
- 0
- Proposed by
- codeseed.app
OpenResty · lua-resty-jwt · Nginx
Project roadmap
- 01
Set up OpenResty
~2hRun OpenResty in Docker (the openresty/openresty image) with your nginx.conf mounted. Add two small backend services (for example tiny Python or Node.js apps returning JSON) in the same Docker Compose file. Declare lua_shared_dict zones for rate limiting, and a location that runs a Lua block returning Hello. Done when: docker compose up starts everything, and the Lua location answers.
- 02
Validate JWTs
~2.5hInstall lua-resty-jwt with opm or LuaRocks. In the access_by_lua_block phase, read the Authorization: Bearer header, verify the signature (HS256 with a secret from an environment variable, or RS256 with a public key), and check the expiry, issuer and audience. Return 401 with a JSON error for missing or invalid tokens, and pass the user id and scopes to the backend as headers. Write a small script to generate test tokens. Done when: valid tokens pass, and expired or tampered ones get 401.
- 03
Limit requests per client
~2.5hImplement rate limiting keyed by the token's client id (or the IP for public routes) using a shared dictionary, with a sliding window or token bucket, or use the lua-resty-limit-traffic library. Use atomic operations such as incr so that all workers share the counts correctly. Return 429 with Retry-After and X-RateLimit headers. Make limits configurable per route. Done when: going over the limit returns 429 no matter which worker handles the request.
- 04
Route to backends
~1.5hDefine upstream blocks for the backends and locations that proxy_pass by path (/users/ to one service, /orders/ to the other), with timeouts, and require specific scopes per route (for example orders:read). Remove the Authorization header before forwarding when backends don't need it. Return a JSON 502 or 504 when a backend fails. Done when: each path reaches the right backend, and missing scopes get 403.
- 05
Add logging and metrics
~1.5hIn the log_by_lua phase, write a JSON access log line with the client id, route, status, decision (allowed, unauthorized, rate limited) and latency, never the token. Count requests by route and decision in a shared dictionary, and expose them on an internal /metrics endpoint that only accepts local requests. Done when: the logs and metrics show every decision.
- 06
Load test and tune
~2hLoad test the gateway on your own machine with wrk or k6, with valid tokens, and compare the latency with calling the backend directly. Try different worker counts and shared dictionary sizes, and cache verified tokens for a short time to avoid repeating signature checks. Record the results in README.md. Done when: README.md shows the throughput and the latency added by the gateway before and after tuning.
- 07
Test it
~2hWrite tests with Test::Nginx (the OpenResty test framework) or a script that runs requests against the Docker setup: valid, expired and tampered tokens, missing scopes, rate limits, routing, and backend failures. Done when: every test case passes against the running gateway.
Resources
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack