Build a One-Key Gateway with Rate Limits & Fallbacks
This guide walks you through building a single-key API gateway that enforces rate limits and provides safe fallbacks when upstream services fail. You’ll create a lightweight proxy that accepts requests with one API key, tracks usage per key, and automatically returns a cached or default response if the backend is down. This is ideal for developers who need a simple, self-contained gateway without heavy infrastructure.
What You’ll Need
- A server or local machine with Node.js (v16+) installed
- npm (comes with Node.js)
- Basic familiarity with JavaScript and Express
- Access to an upstream API endpoint you want to protect (or you can mock one)
- A text editor and terminal
Step-by-Step
- Initialize the project. Create a new directory, navigate into it, and run
npm init -yto generate a package.json file. - Install dependencies. Run
npm install express express-rate-limit. Express is the web framework, and express-rate-limit provides built-in rate limiting middleware. - Create the main file. Create a file named
server.jsin the project root. This will contain all the gateway logic. - Set up Express. In
server.js, require express and create an app instance. Listen on a port (e.g., 3000) and log a message when the server starts. - Define your API key. Store a hardcoded API key in an environment variable or a config file. For simplicity, use a constant like
const API_KEY = 'your-secret-key'. In production, use environment variables. - Create the key validation middleware. Write a middleware function that checks the
x-api-keyheader (or query parameter) against the expected key. If it doesn’t match, return a 401 status with a JSON error message. Otherwise, callnext(). - Apply rate limiting per key. Use
express-rate-limitto create a limiter. SetwindowMs(e.g., 15 minutes),max(e.g., 100 requests), and a customkeyGeneratorthat returns the API key from the header. This ensures each key gets its own limit. Apply this limiter to all routes. - Create a fallback cache. Define a simple in-memory cache object (or use a package like node-cache). For each upstream request, store the response body and timestamp. When the upstream fails, serve the cached response if it’s fresh (e.g., less than 5 minutes old). If no cache exists, return a generic error response with a 503 status.
- Proxy the upstream request. Create a route (e.g.,
/api/*) that forwards the request to your upstream API usingfetch(Node 16+) or axios. Attach the API key to the upstream request if needed. On success, cache the response and send it to the client. - Handle upstream errors. In the proxy route, catch any errors from the upstream call. Log the error, then check the cache. If a cached response exists, return it with a
X-Cache: HITheader. Otherwise, return a 503 with a friendly message. - Test the gateway. Start the server with
node server.jsand send requests using curl or Postman. Test with a valid key, an invalid key, and by stopping the upstream service to verify the fallback works. - Add a health endpoint. Create a
/healthroute that returns 200 OK. This is useful for monitoring and for load balancers to check the gateway’s status.
Common Mistakes
- Not scoping rate limits per key. If you forget the custom
keyGenerator, all users share the same limit, defeating the purpose. - Storing sensitive data in code. Hardcoding the API key in the source is insecure. Use environment variables.
- Ignoring upstream timeouts. If the upstream is slow, your gateway will hang. Set a timeout using AbortController or axios timeout.
- Not handling cache expiry. Without a TTL, cached data becomes stale. Always check the timestamp.
- Logging sensitive headers. Avoid logging the API key itself in error logs.
Tips
- Use a more robust cache like Redis for production to persist data across restarts.
- Add request logging with a library like morgan to track usage patterns.
- Consider using a reverse proxy like Nginx in front of your gateway for additional security.
- Implement a circuit breaker pattern: after several consecutive failures, stop calling the upstream for a cooldown period.
- Return informative error codes (429 for rate limit, 503 for fallback) so clients can handle them appropriately.
Troubleshooting
Rate limit not working? Check that the middleware is applied before your routes. Also verify that the keyGenerator is reading the correct header name.
Fallback not triggering? Ensure you’re catching errors properly. Use a try-catch block and test by intentionally throwing an error in the upstream call.
Requests hanging? Add a timeout to your fetch call. Use AbortSignal.timeout(5000) (Node 17+) or a manual AbortController.
Cache serving stale data? Increase the TTL or implement a background refresh that updates the cache periodically.
API key not validated? Make sure the middleware is placed before the rate limiter and routes. Check that the header name matches exactly (case-insensitive).
Frequently Asked Questions
Can I use this with any upstream API?
Yes, as long as you can make HTTP requests to it. The gateway is protocol-agnostic.
How do I add multiple API keys?
Store keys in an array or a database. Modify the validation middleware to check against the list.
What if the upstream requires authentication?
Forward the appropriate headers (e.g., Authorization) from the client to the upstream, or store a service account credential.
Is in-memory cache enough?
For single-instance deployments, yes. If you run multiple instances, use a shared cache like Redis.
How do I reset a rate limit for a key?
Restart the server or clear the rate limit store. With express-rate-limit, you can use the resetKey method if you have a reference to the limiter.
Can I use this for WebSockets?
This setup is for HTTP requests. For WebSockets, you’d need a different rate-limiting approach, often at the connection level.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.