Node.jsTutorialIP Geolocation
How to Detect User Country from IP in Node.js (2026)
ApogeoAPI5 min read
Detecting a user's country from their IP address is the fastest way to localize your app — no consent forms, no user input. Here's how to do it reliably in Node.js.
Why Detect Country from IP?
- Show prices in the user's local currency automatically
- Pre-select country in forms and phone number inputs
- Enforce geo-restrictions for compliance
- Customize content by region without asking
Basic Express.js Example
import express from 'express';
const app = express();
app.get('/api/session', async (req, res) => {
const ip = req.ip ?? req.socket.remoteAddress ?? '8.8.8.8';
const geoRes = await fetch(
`https://api.apogeoapi.com/v1/geolocate/${ip}`,
{ headers: { 'X-API-Key': process.env.APOGEO_API_KEY! } }
);
const geo = await geoRes.json();
// { country_code: 'DE', country_name: 'Germany', city: 'Berlin',
// timezone: 'Europe/Berlin', currency: 'EUR', latitude: 52.5, longitude: 13.4 }
res.json({ country: geo.country_code, currency: geo.currency, timezone: geo.timezone });
});
Getting the Real IP Behind a Proxy
In production, your app sits behind a reverse proxy (nginx, Cloudflare, load balancer). The raw IP will be the proxy's — not the user's. Use the X-Forwarded-For header:
function getClientIp(req: express.Request): string {
const forwarded = req.headers['x-forwarded-for'];
if (typeof forwarded === 'string') {
return forwarded.split(',')[0].trim(); // First IP is the original client
}
return req.socket.remoteAddress ?? '8.8.8.8';
}
If you're behind Cloudflare, use CF-Connecting-IP header instead — it's more reliable.
Next.js Middleware / Edge Function
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(req: NextRequest) {
const ip = req.headers.get('x-forwarded-for')?.split(',')[0] ?? '8.8.8.8';
const geo = await fetch(
`https://api.apogeoapi.com/v1/geolocate/${ip}`,
{ headers: { 'X-API-Key': process.env.APOGEO_API_KEY! } }
).then(r => r.json());
const res = NextResponse.next();
res.cookies.set('geo_country', geo.country_code, { maxAge: 86400 });
res.cookies.set('geo_currency', geo.currency, { maxAge: 86400 });
return res;
}
export const config = { matcher: ['/((?!_next|api).*)'] };
What the Response Includes
country_code— ISO2 code (e.g. "DE")country_name— Full name (e.g. "Germany")region— State or region namecity— City namelatitude,longitude— Coordinatestimezone— IANA timezone string (e.g. "Europe/Berlin")currency— ISO 4217 currency code (e.g. "EUR")
Caching the Result
IP geolocation data changes infrequently. Cache results in Redis with a 24-hour TTL to avoid hitting your API quota on every request:
const cacheKey = `geo:${ip}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const geo = await fetchGeo(ip);
await redis.setex(cacheKey, 86400, JSON.stringify(geo));
return geo;Try ApogeoAPI free
1,000 requests/month forever. 14-day full-access trial. No credit card.
Get your free API key