The IP API The IP API
// Guide

Get User Location from IP in JavaScript (Browser and Node)

Sep 21st, 2026 // 7 min read
Get User Location from IP in JavaScript (Browser and Node)

If you want JavaScript to get a user's location from their IP, the honest answer has two parts: the lookup itself is a single fetch call to an IP geolocation API, but the call must happen on your server, not in the browser. This guide covers why that is, the small proxy endpoint you should build instead (Express and serverless versions), how to read the visitor's IP correctly behind a proxy, and how IP geolocation differs from navigator.geolocation - because those two get confused constantly.

You'll need an API key for the server-side examples; the free plan at The IP API gives you 1,000 requests per day with no card.

Why Browser JavaScript Can't Get User Location from an IP Directly

It's tempting to drop this straight into your frontend:

// DO NOT ship this - your API key is public the moment this loads
const res = await fetch(
  "https://api.theipapi.com/v1/ip/8.8.8.8?api_key=YOUR_API_KEY"
);

In a browser this fails before the key even becomes a problem: the API doesn't send CORS headers, so a cross-origin fetch from your page is blocked - the browser refuses to hand your script the response because there's no Access-Control-Allow-Origin header on it. That's deliberate. The API is designed to be called from servers, and the missing CORS header is what stops the second, worse problem: anything in browser JavaScript is public. If the call did go through, your API key would ship to every visitor, sit in plain sight in the Network tab of DevTools, and could be lifted and reused by anyone until your quota drained. This applies to every third-party API secret, not just geolocation keys: browser code cannot keep secrets, full stop.

The correct architecture is a thin proxy: the browser calls your endpoint with no key at all, your server calls The IP API with the key from an environment variable, and you return only the fields the frontend needs. As a bonus, your server sees the visitor's IP anyway, so the browser doesn't even need to know its own address. (Curious what yours is? The What Is My IP tool shows exactly the kind of data the API returns for your connection.)

The Building Block: A fetch Lookup in Node 18+

Node has had a global fetch since version 18 (marked stable in 21), so a lookup needs zero dependencies. Save this as lookup.mjs and run IPAPI_KEY=your_key node lookup.mjs:

const API_KEY = process.env.IPAPI_KEY;

async function lookupIp(ip) {
  const res = await fetch(
    `https://api.theipapi.com/v1/ip/${ip}?api_key=${API_KEY}`
  );
  if (!res.ok) {
    // 401 bad key, 429 over quota, 400 malformed request
    throw new Error(`HTTP ${res.status}`);
  }
  const data = await res.json();
  // A valid IP with no matching record returns HTTP 200
  // with "status": "Error" - always check the JSON status field.
  if (data.status !== "OK") {
    throw new Error(`lookup failed: ${data.status}`);
  }
  return data.body;
}

const info = await lookupIp("8.8.8.8");
console.log(info.location.country_code); // "US"
console.log(info.location.city); // "Mountain View"
console.log(info.location.timezone); // "America/Los_Angeles"
console.log(info.is_datacenter); // true

The body also includes location.latitude, location.longitude, location.region, the network's ASN and owning company, and the is_vpn / is_bogon flags. One edge case to know: bogon IPs (private ranges per RFC 1918 like 192.168.0.0/16, loopback, other reserved space) return a minimal body with only ip and is_bogon - no location object at all. Any code that reads info.location should first check it exists, because in local development your visitor IP is usually 127.0.0.1, which is exactly this case.

Reading the Visitor's IP Server-Side

For "where is the person currently on my site", you don't look up an arbitrary IP - you look up the IP of the incoming request. Two rules:

  • With a direct connection, the address is on the socket (req.socket.remoteAddress in Node).
  • Behind a reverse proxy, load balancer, or CDN (nginx, an ALB, Cloudflare), the socket address is the proxy's, and the real client IP arrives in the X-Forwarded-For header as a comma-separated chain. The leftmost entry is the original client.

The catch: X-Forwarded-For is just a header, and clients can send whatever they want in it. Only trust it when your own proxy sets or sanitizes it; if your Node process faces the internet directly, ignore the header and use the socket address.

function clientIp(req) {
  const fwd = req.headers["x-forwarded-for"];
  if (fwd) {
    return fwd.split(",")[0].trim();
  }
  return req.socket.remoteAddress;
}

The Proxy Endpoint: Express Version

Here's the complete pattern (npm install express, Node 18+):

const express = require("express");

const app = express();
const API_KEY = process.env.IPAPI_KEY;

function clientIp(req) {
  const fwd = req.headers["x-forwarded-for"];
  if (fwd) {
    return fwd.split(",")[0].trim();
  }
  return req.socket.remoteAddress;
}

app.get("/api/geo", async (req, res) => {
  try {
    const ip = clientIp(req);
    const apiRes = await fetch(
      `https://api.theipapi.com/v1/ip/${ip}?api_key=${API_KEY}`
    );
    const data = await apiRes.json();

    if (!apiRes.ok || data.status !== "OK" || !data.body.location) {
      return res.json({ ok: false });
    }

    const { country_code, city, timezone } = data.body.location;
    res.json({ ok: true, country: country_code, city, timezone });
  } catch (err) {
    console.error("geo lookup failed:", err.message);
    res.json({ ok: false });
  }
});

app.listen(3000, () => console.log("listening on :3000"));

Two deliberate choices: the endpoint returns only the three fields the frontend needs (never echo the raw upstream response, and never the key), and every failure path collapses to { ok: false } so the frontend has exactly one thing to check. Geolocation is an enhancement - it should degrade, not error.

The browser side is now trivial and contains no secrets:

const res = await fetch("/api/geo");
const geo = await res.json();
if (geo.ok) {
  console.log(`Visitor is in ${geo.city}, ${geo.country}`);
  // preselect country in a form, set currency, pick a locale...
}

If you're building out a fuller Node integration, the Node.js integration guide walks through the API from that angle.

The Proxy Endpoint: Serverless Version

The same pattern fits a serverless function - handy for static sites that have no server of their own. Here's a Vercel-style handler (api/geo.js); Netlify and AWS Lambda equivalents differ only in the handler signature:

export default async function handler(req, res) {
  const fwd = req.headers["x-forwarded-for"];
  const ip = fwd ? fwd.split(",")[0].trim() : req.socket.remoteAddress;

  const apiRes = await fetch(
    `https://api.theipapi.com/v1/ip/${ip}?api_key=${process.env.IPAPI_KEY}`
  );
  const data = await apiRes.json();

  if (!apiRes.ok || data.status !== "OK" || !data.body.location) {
    return res.status(200).json({ ok: false });
  }

  const { country_code, city, timezone } = data.body.location;
  return res.status(200).json({ ok: true, country: country_code, city, timezone });
}

Set IPAPI_KEY in the platform's environment variable settings, never in the repo. On serverless platforms the request has already passed through the platform's proxy layer, so x-forwarded-for is set and sanitized by infrastructure you're implicitly trusting anyway - the leftmost-entry read is the standard approach there.

IP Geolocation vs navigator.geolocation

These solve different problems, and picking the wrong one is the most common mistake in this area.

navigator.geolocation is the browser's Geolocation API. It uses GPS, Wi-Fi positioning, and cell towers, so it can be accurate to a few meters. But it requires a permission prompt the user can (and often does) decline, it's asynchronous with real latency on first fix, and it gives you raw coordinates - turning those into "country" or "city" needs a separate reverse-geocoding step.

IP geolocation needs no permission and no prompt: the IP arrives with the request, so you know the approximate location before rendering anything. Accuracy is coarser - country-level is reliable, city-level is approximate, and VPNs or mobile carriers can shift the apparent location (the is_vpn and is_datacenter flags in the response tell you when to be skeptical).

The rule of thumb: use IP geolocation for country and city-level decisions that must work for every visitor with zero friction - currency, language defaults, content localization, fraud signals. Use navigator.geolocation only when the feature genuinely needs precise coordinates, like "show restaurants within 500 meters", and design for the user saying no. They also compose well: default from IP instantly, then refine with the browser API if the user grants permission.

Wrapping Up

The complete answer to getting user location from an IP in JavaScript: a fetch call to GET /v1/ip/{ip}, made server-side behind a tiny proxy endpoint so your key stays secret, with the visitor's IP read from the socket or a trusted X-Forwarded-For, and navigator.geolocation reserved for the rare cases that need meter-level precision. Full endpoint and response details are in the documentation. To wire it up for real, create a free account - 1,000 requests per day on the free tier, no card required.

Ivan
About the author
Ivan, Founder, The IP API.
// READY_WHEN_YOU_ARE

Ready to start using The IP API?

Unlock accurate, reliable IP geolocation. Integrate in minutes to enhance personalization, performance, and security.