How to build a serverless proxy for a flight data API. Practical patterns for Cloudflare Workers, Vercel Edge Functions and AWS Lambda — API key safety, response caching, rate limiting and cost — with complete code examples using AirLabs.
If you are building a mobile app, a single-page application on any frontend framework, or a lightweight web tool that consumes flight data, you almost certainly have the same problem: the API key cannot ship to the client. Every serious flight data API is paid at the point of production traffic, and every key that ends up in a mobile bundle, a JavaScript file or a browser DevTools tab is a key that will be scraped and used against you within hours. Full backends — Django, Rails, Node with Express — solve the problem but bring an operational overhead that is disproportionate to the actual work: for many applications, the backend does nothing except forward flight queries and hide the key.
Serverless functions at the edge are the natural fit for this shape of problem. A single small function, deployed globally, receives client requests, adds the API key on the server side, forwards the request to AirLabs, caches the response and returns it. There is no server to run, no framework to configure, no build process beyond deploying the function. Cloudflare Workers, Vercel Edge Functions, AWS Lambda + API Gateway and Supabase Edge Functions all support this pattern with minor syntactic differences and identical architecture.
This guide walks through the same proxy built on each of the three most common platforms — Cloudflare Workers, Vercel Edge Functions, AWS Lambda — plus the caching, rate limiting and cost concerns that determine whether the pattern scales in production. Companion guides in this series cover the Python Production Patterns integration used in longer-lived backend services and the Next.js Server Components approach used when the entire application is on Vercel. Serverless is what you reach for when the client is on a different framework, when you need a global cache in front of the API, or when the operational profile of a full backend is more than the workload justifies.
"An edge proxy is a hundred lines of code that fixes an entire category of security bugs. The API key stops being a secret you protect by discipline and becomes a secret the platform makes impossible to leak. That is the trade — small function, global deployment, and the client never touches the credential."
Every serverless proxy for a flight data API implements the same shape:
Every step is small. The interesting design decisions are around what to cache, what to rate-limit, and what to expose. The proxy should be permissive enough that legitimate clients get fast responses, and defensive enough that a leaked proxy URL cannot be used to exhaust the AirLabs plan quota through arbitrary traffic.
The examples below implement the same proxy endpoint — a GET /api/flight/:iata route that returns the status of a specific flight — on three platforms. The pattern extends directly to any AirLabs endpoint: schedules, delays, real-time flights, reference databases.
Cloudflare Workers run on Cloudflare's global edge network, colocated with the caches Cloudflare already operates. For a flight data proxy the pairing is natural: the same platform handles the request routing, the function execution and the response cache.
A minimal working proxy for a single flight lookup:
// worker.js
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const flightIata = url.pathname.split('/').pop();
if (!flightIata) {
return new Response('Missing flight code', { status: 400 });
}
// Check the edge cache first
const cache = caches.default;
const cacheKey = new Request(url.toString(), request);
let response = await cache.match(cacheKey);
if (response) return response;
// Forward to AirLabs with the key on the server side
const airlabsUrl = new URL('https://airlabs.co/api/v9/flight');
airlabsUrl.searchParams.set('flight_iata', flightIata);
airlabsUrl.searchParams.set('api_key', env.AIRLABS_API_KEY);
const upstream = await fetch(airlabsUrl.toString());
const body = await upstream.json();
// Return only the payload; discard error metadata that could hint at the upstream
const payload = body.response ?? {};
response = new Response(JSON.stringify(payload), {
headers: {
'content-type': 'application/json',
'cache-control': 'public, max-age=30',
'access-control-allow-origin': '*',
},
});
// Populate the edge cache for subsequent requests
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return response;
},
};
Deployment is one command through Wrangler, Cloudflare's CLI:
npm install -g wrangler
wrangler init flight-proxy
# Set the secret
wrangler secret put AIRLABS_API_KEY
wrangler deploy
The resulting URL, something like https://flight-proxy.you.workers.dev/api/flight/BA117, is what your mobile app or SPA calls. The AIRLABS_API_KEY is stored encrypted in Cloudflare and injected at execution time; the client sees only the flight payload.
Cloudflare's edge cache, invoked through caches.default, operates per data center — each of Cloudflare's colocations maintains its own cache, so a response cached in Frankfurt does not automatically appear in Los Angeles. In practice this still absorbs the majority of traffic per region because clients in a region tend to hit the same colo. For a globally shared cache — where a single response is reused across all regions — Cloudflare KV or Cache Reserve are the appropriate primitives, at additional cost. For most flight data workloads, per-colo caching is sufficient.
Vercel Edge Functions expose the same execution model — small JavaScript function, global deployment, environment variables injected at runtime — through a slightly different API. If your product is already deployed on Vercel or if you prefer the Next.js hosting ecosystem, this is the natural choice.
Create api/flight/[iata].js:
export const config = {
runtime: 'edge',
};
export default async function handler(request) {
const url = new URL(request.url);
const iata = url.pathname.split('/').pop();
if (!iata) {
return new Response('Missing flight code', { status: 400 });
}
const airlabsUrl = new URL('https://airlabs.co/api/v9/flight');
airlabsUrl.searchParams.set('flight_iata', iata);
airlabsUrl.searchParams.set('api_key', process.env.AIRLABS_API_KEY);
const upstream = await fetch(airlabsUrl.toString());
const body = await upstream.json();
const payload = body.response ?? {};
return new Response(JSON.stringify(payload), {
headers: {
'content-type': 'application/json',
'cache-control': 'public, max-age=30, s-maxage=30',
'access-control-allow-origin': '*',
},
});
}
Deploy through vercel deploy. Set the API key with vercel env add AIRLABS_API_KEY. The endpoint at https://yourproject.vercel.app/api/flight/BA117 is now a working proxy.
Vercel's edge network reads the response Cache-Control header and caches accordingly — s-maxage=30 tells the edge to cache for 30 seconds regardless of browser cache behaviour. Inside a Next.js application, the same result is achievable through next: { revalidate: 30 } on the upstream fetch, but for standalone edge functions the Cache-Control header is the portable, explicit mechanism.
For teams operating on AWS or with existing IAM, logging and monitoring in that ecosystem, Lambda + API Gateway supports the same proxy pattern. The function itself is a plain Node.js handler:
// flight-proxy.mjs
export const handler = async (event) => {
const flightIata = event.pathParameters?.iata;
if (!flightIata) {
return {
statusCode: 400,
headers: { 'content-type': 'text/plain' },
body: 'Missing flight code',
};
}
const url = new URL('https://airlabs.co/api/v9/flight');
url.searchParams.set('flight_iata', flightIata);
url.searchParams.set('api_key', process.env.AIRLABS_API_KEY);
const upstream = await fetch(url.toString());
const body = await upstream.json();
const payload = body.response ?? {};
return {
statusCode: 200,
headers: {
'content-type': 'application/json',
'cache-control': 'public, max-age=30',
'access-control-allow-origin': '*',
},
body: JSON.stringify(payload),
};
};
The AIRLABS_API_KEY is set through the Lambda console or through Infrastructure as Code (SAM, CDK, Terraform). API Gateway routes GET /flight/{iata} to this Lambda.
For response caching, AWS's own CloudFront distribution can sit in front of API Gateway with the cache TTL set through the origin's Cache-Control header — the same convention the Cloudflare and Vercel examples use.
The proxy patterns above route every client request through a serverless function that forwards to AirLabs. There is an official alternative that removes the forwarding step entirely: AirLabs supports temporary signature-based authentication that lets the client call AirLabs directly, without ever seeing the API key.
The mechanism from the AirLabs documentation works like this. Instead of sending an api_key parameter, the client sends a signature parameter constructed on your backend:
signature = api_id : timestamp : md5(timestamp : api_key)
api_id — a public identifier available in response.key.id on any authenticated responsetimestamp — the current Unix timestamp in secondsmd5(...) — MD5 hash of the timestamp concatenated with the API keyThe signature is valid for three minutes. Your backend generates it and returns it to the client; the client uses it directly against AirLabs; the API key never leaves your server, but neither does client traffic flow through your infrastructure.
A minimal signature generator running on a serverless function:
// signature-endpoint.js — runs anywhere (Cloudflare, Vercel, Lambda)
import { createHash } from 'crypto';
export default async function handler(request, env) {
const apiKey = env.AIRLABS_API_KEY;
const apiId = env.AIRLABS_API_ID;
const timestamp = Math.floor(Date.now() / 1000);
const hash = createHash('md5')
.update(`${timestamp}:${apiKey}`)
.digest('hex');
return new Response(
JSON.stringify({
signature: `${apiId}:${timestamp}:${hash}`,
expires_at: timestamp + 180,
}),
{
headers: {
'content-type': 'application/json',
'cache-control': 'no-store',
'access-control-allow-origin': '*',
},
},
);
}
The client fetches a fresh signature every three minutes and uses it against AirLabs directly:
// Client code (browser or mobile app)
let currentSignature = null;
let signatureExpiresAt = 0;
async function getSignature() {
const now = Math.floor(Date.now() / 1000);
if (currentSignature && signatureExpiresAt > now + 10) {
return currentSignature;
}
const res = await fetch('https://yourdomain.com/api/signature');
const data = await res.json();
currentSignature = data.signature;
signatureExpiresAt = data.expires_at;
return currentSignature;
}
async function getFlight(flightIata) {
const signature = await getSignature();
const url = `https://airlabs.co/api/v9/flight?flight_iata=${flightIata}&signature=${signature}`;
const res = await fetch(url);
return (await res.json()).response;
}
When to prefer signature over a full proxy:
When a full proxy is still the right answer:
Many production deployments end up using both patterns: a signature endpoint for high-frequency real-time queries where latency matters, and a proxy for cached reference data (airports, airlines, fleets) where the cache absorbs most of the load.
Every serverless flight proxy needs a caching story. Uncached, every client request becomes an AirLabs request; the proxy adds latency and cost without reducing load. Cached correctly, the proxy absorbs the vast majority of traffic at the edge and only the cache-miss rate flows through to the upstream API.
Three caching layers commonly compose:
Cache-Control headers. This is what cache-control: public, max-age=30 in the examples above activates. Every subsequent identical request within 30 seconds is served from the edge cache without invoking the function again.Map. Useful for high-cardinality endpoints where even a few seconds of caching per instance reduces upstream calls.For flight data specifically, the caching TTL depends on the endpoint:
Aggressive caching of reference data is the single largest reduction in effective API request rate you can make. An airports?iata_code=LHR request cached for an hour serves potentially thousands of client lookups on one AirLabs call.
The other production concern is rate limiting inbound to your proxy. Without it, a leaked proxy URL — or a client with a runaway bug — can send unbounded traffic that exhausts your AirLabs plan quota. AirLabs enforces three levels of quota which surface as distinct error codes: minute_limit_exceeded, hour_limit_exceeded and month_limit_exceeded. Your proxy should defend against all three by rate-limiting inbound requests before they reach the upstream call.
A minimal per-IP rate limiter in Cloudflare Workers using KV:
async function checkRateLimit(env, clientIp) {
const key = `rl:${clientIp}`;
const currentRaw = await env.RATE_LIMIT_KV.get(key);
const count = currentRaw ? parseInt(currentRaw, 10) : 0;
const LIMIT_PER_MINUTE = 60;
if (count >= LIMIT_PER_MINUTE) return false;
await env.RATE_LIMIT_KV.put(key, String(count + 1), {
expirationTtl: 60,
});
return true;
}
// In the fetch handler:
const clientIp = request.headers.get('CF-Connecting-IP') || 'unknown';
const allowed = await checkRateLimit(env, clientIp);
if (!allowed) {
return new Response('Rate limit exceeded', { status: 429 });
}
For Vercel, the equivalent uses Vercel KV; for AWS, DynamoDB with TTL. The pattern is the same — increment a counter keyed by client identity, expire after the window, reject when the counter exceeds the limit.
For an authenticated proxy, rate-limit by client token rather than by IP. Mobile apps behind carrier NAT and shared corporate networks can all originate from the same IP, and blocking by IP unfairly locks out entire cohorts.
Frontend applications served from a different origin than the proxy need CORS headers. Every example above sets access-control-allow-origin: * to allow any browser origin; production deployments typically restrict this to specific origins your application uses.
For a browser-based single-page app:
// In the client
const response = await fetch('https://flight-proxy.yourdomain.com/api/flight/BA117');
const flight = await response.json();
console.log(flight.status, flight.dep_iata, flight.arr_iata);
For a native mobile app:
// React Native / Flutter / iOS / Android
const response = await fetch('https://flight-proxy.yourdomain.com/api/flight/BA117');
const flight = await response.json();
The pattern is identical because the proxy exposes a plain REST endpoint. The proxy URL is the only credential your client needs, and it references no secret material.
For a small-to-medium flight data application, the edge functions themselves are inexpensive to the point of being effectively free on the generous startup tiers:
The cost that matters is the AirLabs plan cost, which scales with your cache miss rate rather than with client requests. A well-cached proxy serving one million client requests to a service where the cache hit rate is 95% only makes 50,000 upstream AirLabs calls. Sizing the AirLabs plan against the cache miss rate — not the raw client traffic — is what makes the pattern work economically at scale.
Being clear about scope avoids surprises. Serverless proxies solve API key safety, response caching and edge distribution — they do not solve every concern of a production flight data application:
listener_id, active flights, associated users) must live in a persistent database. Serverless functions are stateless by design.Cache-Control headers on every response. Even a 15-second cache reduces upstream load meaningfully at scale._fields on the upstream request. The AirLabs _fields parameter reduces the size of the upstream response, which reduces both bandwidth and cache size.If you are shipping a mobile app, a Vue or Svelte SPA, an internal dashboard on any framework, or any client that cannot safely hold a paid API key, a serverless proxy in front of the AirLabs API is the shortest path from a working prototype to a production-grade deployment. The pattern is the same across Cloudflare Workers, Vercel Edge Functions and AWS Lambda: a small function, the API key on the server side, cached responses at the edge, rate limiting to protect the quota. Deployed globally, it turns AirLabs into an infinitely-scalable data source your client applications can call without ever holding a secret.
Our Developer API allows you to create a custom experience for your users and increase the value of your product:
_fields for lean, targeted responsesYou can try it right now without any obligation! Get a free flight API plan and see for yourself that we have exactly the data you need!
If you need more information, don't hesitate to contact us. We are always happy to chat with our customers and are sure to find a customized solution for each request.
Explore AirLabs, or create an account instantly and start using API.
Get FREE API Key