AWS LambdaServerlessIP GeolocationNode.jsTutorial
IP Geolocation in AWS Lambda with Node.js: Country Detection Guide
ApogeoAPI6 min read
Getting the caller's IP in AWS Lambda
The IP extraction method depends on how your Lambda is triggered:
// For API Gateway (REST or HTTP API)
function getIP(event: APIGatewayProxyEvent): string {
// API Gateway HTTP API v2
if ('requestContext' in event && 'http' in (event as any).requestContext) {
return (event as any).requestContext.http.sourceIp;
}
// API Gateway REST API v1
return event.requestContext?.identity?.sourceIp
?? event.headers?.['X-Forwarded-For']?.split(',')[0].trim()
?? '127.0.0.1';
}
// For Application Load Balancer
function getIPFromALB(event: ALBEvent): string {
return event.headers?.['x-forwarded-for']?.split(',')[0].trim()
?? '127.0.0.1';
}
// For Lambda URL (direct invocation)
function getIPFromFunctionURL(event: APIGatewayProxyEventV2): string {
return event.requestContext.http.sourceIp;
}
Basic geolocation handler
// handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
const APOGEO_KEY = process.env.APOGEO_KEY!;
const BASE_URL = 'https://api.apogeoapi.com/v1';
interface GeoData {
countryCode: string;
countryName: string;
city: string;
regionName: string;
timezone: string;
currencyCode: string;
currencySymbol: string;
}
// Module-level cache — persists across Lambda invocations in the same container
const geoCache = new Map<string, { data: GeoData; expires: number }>();
async function geolocate(ip: string): Promise<GeoData> {
const subnet = ip.split('.').slice(0, 3).join('.') + '.0';
const cached = geoCache.get(subnet);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
const res = await fetch(
`${BASE_URL}/geo/${ip}?apikey=${APOGEO_KEY}`,
{ signal: AbortSignal.timeout(2000) }
);
if (!res.ok) {
return { countryCode: 'US', countryName: 'United States', city: '',
regionName: '', timezone: 'America/New_York',
currencyCode: 'USD', currencySymbol: '$' };
}
const data: GeoData = await res.json();
geoCache.set(subnet, { data, expires: Date.now() + 3_600_000 });
return data;
}
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
const ip = event.requestContext?.identity?.sourceIp
?? event.headers?.['X-Forwarded-For']?.split(',')[0].trim()
?? '127.0.0.1';
const geo = await geolocate(ip);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'private, max-age=3600',
},
body: JSON.stringify({ ip, ...geo }),
};
};
Lambda environment variable setup
# Using AWS CLI
aws lambda update-function-configuration \
--function-name my-geo-function \
--environment "Variables={APOGEO_KEY=your_api_key}"
# Using SAM template.yml
Environment:
Variables:
APOGEO_KEY: !Sub "{{resolve:ssm:/app/apogeo-key}}"
# Using Serverless Framework (serverless.yml)
environment:
APOGEO_KEY: ${ssm:/app/apogeo-key}
Localized pricing Lambda
// pricing-handler.ts
const PLANS = {
basic: 19,
starter: 29,
professional: 79,
};
const PPP_FACTORS: Record<string, number> = {
IN: 0.35, BR: 0.55, MX: 0.60, AR: 0.40,
};
export const handler = async (event: APIGatewayProxyEvent) => {
const ip = event.requestContext?.identity?.sourceIp ?? '127.0.0.1';
const geo = await geolocate(ip);
const ppp = PPP_FACTORS[geo.countryCode] ?? 1;
// Fetch live exchange rate from ApogeoAPI
const rateRes = await fetch(
`https://api.apogeoapi.com/v1/rates/USD?apikey=${APOGEO_KEY}`,
{ signal: AbortSignal.timeout(2000) }
);
const rateData = await rateRes.json();
const rate = rateData.rates?.[geo.currencyCode] ?? 1;
const prices = Object.fromEntries(
Object.entries(PLANS).map(([plan, usd]) => [
plan,
{
usd,
local: Math.round(usd * ppp * rate),
currency: geo.currencyCode,
symbol: geo.currencySymbol,
hasPPP: ppp !== 1,
},
])
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
// Cache at CloudFront by country — each edge node caches for 1h
'Cache-Control': 'public, max-age=3600',
'Vary': 'CloudFront-Viewer-Country',
},
body: JSON.stringify({ country: geo.countryCode, prices }),
};
};
Lambda@Edge for CloudFront
Lambda@Edge runs at CloudFront edge nodes — it receives geo headers CloudFront injects automatically:
// cloudfront-geo-handler.ts (origin-request trigger)
import { CloudFrontRequestEvent, CloudFrontRequestResult } from 'aws-lambda';
export const handler = async (
event: CloudFrontRequestEvent
): Promise<CloudFrontRequestResult> => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// CloudFront injects these automatically (no API call needed)
const country = headers['cloudfront-viewer-country']?.[0]?.value ?? 'US';
const city = headers['cloudfront-viewer-city']?.[0]?.value ?? '';
// Add geo context to the origin request
request.headers['x-viewer-country'] = [{ key: 'X-Viewer-Country', value: country }];
request.headers['x-viewer-city'] = [{ key: 'X-Viewer-City', value: city }];
// If we need currency data, fetch from ApogeoAPI (cache result in-memory)
// Lambda@Edge has a 1MB package limit — no node_modules, use fetch directly
return request; // forward to origin
};
Caching strategy for Lambda
Lambda containers are reused across invocations. Module-level variables persist between invocations in the same container:
- Warm container: cache hit, 0ms added latency
- Cold start: new container, cache miss, one API call (~20-50ms)
- Cache key: /24 subnet (8.8.8.0) so one office network = 1 lookup ever
For even better performance, use ElastiCache Redis as a shared cache across all Lambda instances in your VPC.
SAM template example
# template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 10
MemorySize: 256
Runtime: nodejs20.x
Environment:
Variables:
APOGEO_KEY: !Sub "{{resolve:ssm:/prod/apogeo-key}}"
Resources:
PricingFunction:
Type: AWS::Serverless::Function
Properties:
Handler: dist/pricing-handler.handler
Events:
Api:
Type: HttpApi
Properties:
Path: /api/pricing
Method: GET
Full API docs: api.apogeoapi.com/api/docs. Get a free API key: app.apogeoapi.com/register.
Try ApogeoAPI free
1,000 requests/month forever. 14-day full-access trial. No credit card.
Get your free API key