Building an EAI Middleware Layer
The Gatekeeper Doesn't Do the Work Inside
In one line
An API gateway stands in front of HTTP APIs and decides in one place who (authentication), how much (quota) and which version (routing) is calling. If an EAI hub connects systems by translating messages, the gateway plays only the gatekeeper of calls, without knowing the business logic. Whether you use a product (Kong, Apigee and so on) or build it with nginx, the principle of what it does is the same.
Why it was needed
Say a bank opened a lookup API to partners and fintechs. At first the API server itself checked keys, counted calls and redirected old-version requests to the new version. When the API servers grew to three, the key-checking code in the three places drifted slightly apart. When one partner poured out thousands of requests per second because of a bug, all partners slowed down together. When we tried to take v1 down, nobody knew who was still using v1.
These three — authentication, call rate limits and versions — are needed identically by every API regardless of the business. So instead of implementing them in every API server, you gather them in one place in front. Conversely, if you start putting business rules (balance checks, limit calculations) into the gateway, the gateway becomes a second application server. The gatekeeper looks at your identity and opens the door; it does not do the work inside the room.
How it works
This lab builds the principle with nginx's basic modules, without a product. It uses only directives from the official documentation.
Authentication — the API key as a consumer name. map is a table that turns a request value (here the X-API-Key header, the variable $http_x_api_key) into another variable. You keep the key → consumer name table in a file and include it. A key not in the table gets the default (an empty value), and an empty consumer is returned with 401. To the upstream you pass only the consumer name as X-Consumer and not the key itself — if you give an empty string as the value of proxy_set_header, that header is not passed to the upstream. The secret stops at the gateway, and the key does not leak into the upstream logs. (An API key identifies the calling program and does not authenticate a user. If you need per-user permissions, you use tokens — outside the scope of this course.)
Quota — per-consumer rate limit. The limit_req module, in the documentation's words, limits the request processing rate with the "leaky bucket" method. If you create a zone with limit_req_zone $consumer zone=… rate=5r/s, keyed by consumer name, the limit is applied separately to each consumer (if you key by IP, all the partners behind one NAT share one bucket). According to the documentation, requests with an empty key are not counted. burst decides how many momentarily piled-up requests to queue, and nodelay decides whether to process queued requests immediately without delaying them. A request over the limit gets 503 by default, and you change it with limit_req_status 429 — 429 Too Many Requests is a status code RFC 6585 defined to mean "you have sent too many requests," so the caller can tell "the server is ailing (503)" from "I sent too much (429)."
Version — path and header. Putting the version in the path (/v1/…, /v2/…) is visible and easy to tell apart in caches and logs. Choosing by header (X-API-Version: 2) does not change the path. This lab accepts both: /api/… without a version is chosen by header, and if there is no header it is v1. And to the version to be taken down, you attach the Sunset header (RFC 8594) to announce "this resource may not respond after this time." The value is in HTTP date format. Since it is attached to every response, the caller's logs and monitoring notice on their own — more reliable than an announcement email.
Request ID. If the caller sent X-Request-ID, pass it on as is, and if not, the gateway makes one. nginx's $request_id is, per the documentation, a unique identifier written as 16 random bytes in hexadecimal. If you leave this ID in the access log together with the consumer and status, when a partner says "I got a 429 yesterday at 14:00," you can find that line right away (the same idea as module 7's GUID, applied at the HTTP boundary).
Timeout. If the upstream stalls, the gateway's connection stalls with it. proxy_read_timeout is the maximum time to wait between two reads from the upstream, and if exceeded, the gateway returns 504. The default is 60 seconds, but for an API where the caller gives up in 10 seconds, there is no reason for the gateway to hold on for 60 seconds (module 4's "shorter going inward").
How does it differ from the EAI hub? The hub translates messages (format, codes), switches between sync and async, and composes several systems. The gateway passes HTTP requests through almost as they are and does only control. In the field the two exist together — external partner → gateway → hub → core banking.
What it looks like in the field
First, the mistake of applying quotas by IP. A large partner runs several services behind a NAT and they eat each other's limits. Consumer identification comes first. Second, a setting that returns quota overruns as 503. The caller's retry logic sees it as a "server outage" and retries harder. Third, API keys remaining in the upstream logs — partner keys pile up in plaintext in the log collection system. Fourth, announcing version retirement only by email. A partner whose contact person changed does not know and suffers an outage on the retirement day.
What we do in the next lab
You start the upstream fixtures (v1 and v2, an API that mirrors the headers it received) and grow the nginx configuration nginx.conf step by step — a path proxy, API key authentication and passing the consumer name, per-consumer quotas and 429, header-based version routing and Sunset, request ID and access log, and the upstream timeout. The grader copies your configuration file and starts it afresh on its own port and sends requests through.