Node.js
This tutorial shows how to use JavaScript to look up IP geolocation data — country, city, latitude, longitude, and timezone — using Node.js and the ip-api.io API. Since the API requires an API key, all lookups happen server-side. We cover Express.js middleware, Next.js API routes, and TypeScript types. Browser-side code should call your own backend, not ip-api.io directly.
What you'll build
fetch()
fetch — no npm install required)IP_API_IO_KEYawait, which requires "type": "module" in your
package.json, or save the file with a .mjs extension.
If you prefer CommonJS, wrap the code in an async function and call it.
Pass any IPv4 or IPv6 address to GET /api/v1/ip/{ip}.
The response includes geolocation and security signals in a single call.
Store your API key in an environment variable — never hardcode it.
const API_KEY = process.env.IP_API_IO_KEY;
const ip = "8.8.8.8";
const resp = await fetch(
`https://ip-api.io/api/v1/ip/${ip}?api_key=${API_KEY}`,
{ signal: AbortSignal.timeout(5000) }
);
if (!resp.ok) throw new Error(`ip-api.io error: ${resp.status}`);
const data = await resp.json();
console.log(data.ip); // "8.8.8.8"
console.log(data.location.country); // "United States"
console.log(data.location.city); // "Mountain View"
console.log(data.location.latitude); // 37.4056
console.log(data.location.longitude); // -122.0775
console.log(data.location.timezone); // "America/Los_Angeles"
console.log(data.location.local_time); // "2024-05-20T15:16:52-07:00"
GET /api/v1/ip/?api_key=YOUR_KEY — and the API resolves it from
the incoming request. Useful when calling server-side with the client IP
extracted from X-Forwarded-For.
Every response includes a suspicious_factors object with seven boolean flags.
You get security intelligence in the same call, at no extra cost per lookup.
const data = await lookupIp("185.220.101.45"); // example Tor exit node
const sf = data.suspicious_factors;
if (sf.is_vpn || sf.is_proxy || sf.is_tor_node) {
console.log("Anonymous or masked traffic — consider blocking or CAPTCHA");
}
if (sf.is_datacenter) {
console.log("Cloud/datacenter IP — likely automated or bot traffic");
}
if (sf.is_threat) {
console.log("Active threat signal — block this request");
}
// Combine signals for a risk decision
const isRisky = sf.is_vpn || sf.is_proxy || sf.is_tor_node || sf.is_threat;
console.log(`Risk flag: ${isRisky}`); // true
VERY_LOW / LOW / MEDIUM / HIGH /
VERY_HIGH risk level, use the
Risk Score API at
/api/v1/risk-score/{ip}.
Copy this interface into your project to get full type safety on every field. No external type package needed.
interface SuspiciousFactors {
is_vpn: boolean;
is_proxy: boolean;
is_tor_node: boolean;
is_datacenter: boolean;
is_threat: boolean;
is_spam: boolean;
is_crawler: boolean;
}
interface Location {
country: string | null;
country_code: string | null;
city: string | null;
latitude: number | null;
longitude: number | null;
zip: string | null;
timezone: string | null;
local_time: string | null;
local_time_unix: number | null;
is_daylight_savings: boolean | null;
}
interface IpApiResponse {
ip: string;
suspicious_factors: SuspiciousFactors;
location: Location;
}
// Typed lookup function
async function lookupIp(ip: string): Promise<IpApiResponse> {
const apiKey = process.env.IP_API_IO_KEY!;
const resp = await fetch(
`https://ip-api.io/api/v1/ip/${ip}?api_key=${apiKey}`,
{ signal: AbortSignal.timeout(5000) }
);
if (!resp.ok) throw new Error(`ip-api.io error: ${resp.status}`);
return resp.json() as Promise<IpApiResponse>;
}
Attach IP intelligence to every request as req.ipIntel.
The middleware never crashes — if ip-api.io is unavailable, it sets
req.ipIntel = null and calls next().
Treat ip-api.io as a non-critical enrichment, not a hard dependency.
import express from "express";
const API_KEY = process.env.IP_API_IO_KEY;
// Extract the real client IP from X-Forwarded-For (set by reverse proxies).
// Only trust this header when behind a reverse proxy you control —
// an attacker can spoof it on a directly-exposed server.
function getClientIp(req) {
const xff = req.headers["x-forwarded-for"];
return xff ? xff.split(",")[0].trim() : req.socket.remoteAddress;
}
async function ipIntelMiddleware(req, res, next) {
try {
const ip = getClientIp(req);
const resp = await fetch(
`https://ip-api.io/api/v1/ip/${ip}?api_key=${API_KEY}`,
{ signal: AbortSignal.timeout(5000) }
);
req.ipIntel = resp.ok ? await resp.json() : null;
} catch {
req.ipIntel = null;
}
next();
}
const app = express();
app.use(ipIntelMiddleware);
app.get("/check", (req, res) => {
const data = req.ipIntel;
if (!data) return res.status(503).json({ error: "IP lookup unavailable" });
res.json({
ip: data.ip,
country: data.location.country,
is_vpn: data.suspicious_factors.is_vpn,
is_proxy: data.suspicious_factors.is_proxy,
is_tor: data.suspicious_factors.is_tor_node,
is_threat: data.suspicious_factors.is_threat,
});
});
app.listen(3000);
app.set('trust proxy', 1) to your Express app and use
req.ip directly — Express will populate it from
X-Forwarded-For automatically.
Create a server-side API route so your frontend can call
/api/ip-lookup on your own domain. The API key stays in
environment variables and is never sent to the browser.
// app/api/ip-lookup/route.ts (Next.js App Router)
import { NextRequest, NextResponse } from "next/server";
const API_KEY = process.env.IP_API_IO_KEY!;
export async function GET(request: NextRequest): Promise<NextResponse> {
const xff = request.headers.get("x-forwarded-for");
const clientIp = xff ? xff.split(",")[0].trim() : "127.0.0.1";
try {
const resp = await fetch(
`https://ip-api.io/api/v1/ip/${clientIp}?api_key=${API_KEY}`,
{ signal: AbortSignal.timeout(5000) }
);
if (!resp.ok) {
return NextResponse.json({ error: "IP lookup failed" }, { status: 502 });
}
const data = await resp.json();
return NextResponse.json(data);
} catch {
return NextResponse.json({ error: "IP lookup unavailable" }, { status: 503 });
}
}
fetch('/api/ip-lookup') — they never touch ip-api.io directly
and never see the API key. Add export const runtime = 'edge'
to run this route on Vercel Edge for lower latency.
Every IP lookup returns a JSON object with three top-level keys.
All location fields are nullable — they may be null for private ranges
or unrecognized addresses.
{
"ip": "78.55.53.58",
"suspicious_factors": {
"is_proxy": false,
"is_tor_node": false,
"is_spam": false,
"is_crawler": false,
"is_datacenter": false,
"is_vpn": false,
"is_threat": false
},
"location": {
"country": "Germany",
"country_code": "DE",
"city": "Berlin",
"latitude": 52.5694,
"longitude": 13.3753,
"zip": "13409",
"timezone": "Europe/Berlin",
"local_time": "2024-05-20T22:16:52+02:00",
"local_time_unix": 1716236212,
"is_daylight_savings": true
}
}
suspicious_factors — security signals (all boolean)
| Field | Description |
|---|---|
is_vpn |
VPN service, corporate gateway, or self-hosted VPN detected |
is_proxy |
HTTP, HTTPS, or SOCKS proxy (~99.5% accuracy) |
is_tor_node |
Tor exit node, relay, or bridge (updated in real time from Tor consensus) |
is_datacenter |
Cloud provider (AWS, GCP, Azure), VPS, or hosting facility |
is_threat |
Active security threat — malware C&C, botnet, or DDoS source |
is_spam |
Associated with spam, phishing, or malware email campaigns |
is_crawler |
Known web crawler, scraper, or bot (search engines, price monitors) |
location — geographic and timezone data
| Field | Type | Description |
|---|---|---|
country |
String | Full country name in English (ISO 3166-1, e.g. "Germany") |
country_code |
String | ISO 3166-1 alpha-2 code (e.g. "DE", "US", "GB") |
city |
String | City or municipality name (85–95% accuracy) |
latitude |
Float | Decimal degrees, WGS84 — ~50 km median accuracy radius |
longitude |
Float | Decimal degrees, WGS84 — ~50 km median accuracy radius |
zip |
String | Postal or ZIP code in country-specific format |
timezone |
String | IANA timezone identifier (e.g. "America/Los_Angeles") |
local_time |
String | Current local time in ISO 8601 with UTC offset |
local_time_unix |
Integer | Unix timestamp (seconds since epoch) in local timezone |
is_daylight_savings |
Boolean | True if the location is currently observing DST; null if not applicable |
| Field | Type | Description |
|---|---|---|
ip |
String | Analyzed IP address in normalized format (IPv4 dotted decimal or compressed IPv6) |
suspicious_factors |
Object | Seven boolean security signals — always present, never null |
location |
Object | Geographic and timezone data — fields may be null for private/unrecognized IPs |
Common questions about using the JavaScript and Node.js geolocation API.
No. Calling ip-api.io directly from the browser would expose your API key in the browser's network tab — anyone who sees it can use your quota. All calls must go through your server.
The correct pattern: your frontend calls /api/ip-lookup (or any path
you choose) on your own domain. Your server calls ip-api.io with the API
key from an environment variable and returns the result. See the
Next.js example and
Express middleware above.
No. Node.js 18 and higher ships with a built-in fetch() function —
the same API as the browser. No npm install required.
If you are on Node.js 16, install node-fetch as a drop-in replacement:
npm install node-fetch. For HTTP clients like axios,
the same endpoint and query-param pattern works — just replace fetch
with axios.get(url).
Behind a reverse proxy (nginx, Cloudflare, AWS ALB), the real client IP is in the
X-Forwarded-For header, not req.socket.remoteAddress.
Extract it manually:
const xff = req.headers["x-forwarded-for"];
const clientIp = xff ? xff.split(",")[0].trim() : req.socket.remoteAddress;
Or add app.set('trust proxy', 1) to your Express app — Express will
populate req.ip from X-Forwarded-For automatically.
Only enable this when you are actually behind a trusted reverse proxy.
Yes. The API returns standard JSON, and you can define TypeScript interfaces for full
type safety. Copy the IpApiResponse interface from
Step 3 into your project — it covers
suspicious_factors, location, and all nested fields with
correct nullability.
Use AbortSignal.timeout(5000) to set a 5-second timeout. Always check
resp.ok before calling .json(). Wrap the whole call in
try/catch to handle network failures:
try {
const resp = await fetch(url, { signal: AbortSignal.timeout(5000) });
if (!resp.ok) { /* handle HTTP error */ }
const data = await resp.json();
} catch (err) {
// TimeoutError, TypeError (network fail), or any fetch error
// Proceed without IP intelligence — treat it as non-critical
}
In production, treat ip-api.io as a non-critical enrichment. If the request fails, log the error and let the user through — never block a request because of a failed IP lookup.
Yes. The Express middleware pattern translates directly to other frameworks:
fastify.addHook('onRequest', ...)app.use('*', async (c, next) => { ... })app.use(async (ctx, next) => { ... })
In each case, call ip-api.io at the start of the middleware, attach the result to the
context or request object, and call next(). Always catch errors and
continue without crashing.
Create a server-side API route. In the App Router, create
app/api/ip-lookup/route.ts and use the Request object
to read the x-forwarded-for header. See the full example in
Step 5.
Your React components call fetch('/api/ip-lookup') — they never touch
ip-api.io directly. The API key lives in .env.local as
IP_API_IO_KEY and is never sent to the browser.
Start using IP-API.io to make your website safer and more user-friendly. Keep out unwanted bots, show visitors content that's relevant to where they are, and spot risky IP addresses quickly. It's perfect for making online shopping more personal and keeping your site secure. Get started today with one of the plans!
Explore how IP-API.io can enhance your security, provide robust bot protection, and improve IP geolocation accuracy for your applications.
Contact SupportCustomize your experience with tailored plans that fit your IP security and geolocation needs.
Email Us