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 name
  • city — City name
  • latitude, longitude — Coordinates
  • timezone — 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