Looking up IP address geolocation in Node.js is quick and simple with ipinfo.im — a free IP API that requires no authentication. This guide covers two popular HTTP client approaches (native fetch and axios), proper async/await patterns, error handling, and a real-world Express.js middleware example.
Prerequisites
- Node.js 18 or later (for native
fetchsupport) - Optional:
axios(npm install axios)
No API key. No signup. Just code.
Method 1: Using Native fetch (Node.js 18+)
Node.js 18 added native fetch, so you can call the API without any extra dependencies:
// ip-lookup.js
async function lookupIP(ip) {
const url = ip
? `https://ipinfo.im/api/?ip=${encodeURIComponent(ip)}`
: 'https://ipinfo.im/api/';
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
// Look up a specific IP
const data = await lookupIP('8.8.8.8');
console.log(`Country: ${data.country}`);
console.log(`City: ${data.city}`);
console.log(`ISP: ${data.org}`);
console.log(`Timezone: ${data.timezone}`);
Output:
Country: US
City: Mountain View
ISP: AS15169 Google LLC
Timezone: America/Los_Angeles
Look up your own IP:
const myData = await lookupIP(null);
console.log(`My IP: ${myData.ip}`);
console.log(`My location: ${myData.city}, ${myData.country}`);
Method 2: Using axios
If you prefer axios for its automatic JSON parsing, interceptors, and timeout handling:
npm install axios
// ip-lookup-axios.js
import axios from 'axios';
const ipClient = axios.create({
baseURL: 'https://ipinfo.im/api/',
timeout: 10000,
headers: {
'Accept': 'application/json',
'User-Agent': 'MyApp/1.0',
},
});
async function lookupIP(ip) {
const params = ip ? { ip } : {};
const { data } = await ipClient.get('', { params });
return data;
}
// Usage
try {
const info = await lookupIP('1.1.1.1');
console.log(`IP: ${info.ip}`);
console.log(`Country: ${info.country_name} (${info.country})`);
console.log(`City: ${info.city}, ${info.region}`);
console.log(`ISP: ${info.org}`);
} catch (error) {
if (error.response) {
console.error(`API error: ${error.response.status}`);
} else if (error.code === 'ECONNABORTED') {
console.error('Request timed out');
} else {
console.error(`Network error: ${error.message}`);
}
}
Robust Async/Await Helper with Error Handling
For production code, wrap the lookup in a helper that gracefully handles failures:
// ipinfo-client.js
const IPINFO_BASE = 'https://ipinfo.im/api/';
/**
* Look up an IP address using the ipinfo.im API.
* @param {string|null} ip - IP to look up, or null for the caller's own IP.
* @returns {Promise<Object|null>} IP data object, or null on failure.
*/
export async function lookupIP(ip = null) {
const url = ip
? `${IPINFO_BASE}?ip=${encodeURIComponent(ip)}`
: IPINFO_BASE;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch(url, {
signal: controller.signal,
headers: { Accept: 'application/json' },
});
if (!response.ok) {
console.warn(`ipinfo.im returned ${response.status} for IP: ${ip}`);
return null;
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.warn(`Timeout looking up IP: ${ip}`);
} else {
console.error(`Error looking up IP ${ip}: ${error.message}`);
}
return null;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Get just the country code for an IP.
* @param {string} ip
* @returns {Promise<string|null>} ISO country code or null.
*/
export async function getCountry(ip) {
const data = await lookupIP(ip);
return data?.country ?? null;
}
Usage:
import { lookupIP, getCountry } from './ipinfo-client.js';
const info = await lookupIP('8.8.8.8');
if (info) {
console.log(`${info.ip} is in ${info.city}, ${info.country}`);
}
const country = await getCountry('8.8.8.8');
console.log(country); // "US"
Batch IP Lookups
When processing multiple IPs, use sequential requests with a small delay:
/**
* Look up multiple IPs sequentially.
* @param {string[]} ips - Array of IP addresses.
* @param {number} delayMs - Milliseconds between requests.
* @returns {Promise<Map<string, Object|null>>}
*/
async function batchLookup(ips, delayMs = 200) {
const results = new Map();
for (const ip of ips) {
try {
const response = await fetch(`https://ipinfo.im/api/?ip=${ip}`);
const data = response.ok ? await response.json() : null;
results.set(ip, data);
} catch {
results.set(ip, null);
}
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
return results;
}
// Example
const ips = ['8.8.8.8', '1.1.1.1', '9.9.9.9'];
const results = await batchLookup(ips);
for (const [ip, data] of results) {
if (data) {
console.log(`${ip.padEnd(18)} ${data.country} | ${data.city} | ${data.org}`);
}
}
Express.js Middleware: Geo-Aware Responses
Here’s a real-world pattern — an Express.js middleware that enriches each request with geographic data and returns region-specific content:
// server.js
import express from 'express';
const app = express();
// ---- IP Geo Lookup ----
async function getIPCountry(ip) {
// Skip private/local IPs
if (!ip || ip === '::1' || ip.startsWith('192.168') || ip.startsWith('10.')) {
return 'XX';
}
try {
const resp = await fetch(`https://ipinfo.im/api/?ip=${ip}`, {
headers: { Accept: 'application/json' },
});
if (!resp.ok) return 'XX';
const data = await resp.json();
return data.country || 'XX';
} catch {
return 'XX';
}
}
// ---- Middleware ----
app.use(async (req, res, next) => {
// Respect X-Forwarded-For from reverse proxies
const forwardedFor = req.headers['x-forwarded-for'];
const clientIP = forwardedFor
? forwardedFor.split(',')[0].trim()
: req.socket.remoteAddress;
req.geoCountry = await getIPCountry(clientIP);
next();
});
// ---- Routes ----
app.get('/greeting', (req, res) => {
const country = req.geoCountry;
const greetings = {
US: 'Hello! Welcome to our service.',
GB: 'Hello! Welcome to our service.',
DE: 'Hallo! Willkommen bei unserem Service.',
JP: 'こんにちは!私たちのサービスへようこそ。',
FR: 'Bonjour! Bienvenue sur notre service.',
};
const message = greetings[country] || 'Hello! Welcome to our service.';
res.json({ country, message });
});
app.get('/pricing', (req, res) => {
const country = req.geoCountry;
// Different pricing by region
const pricing = {
US: { currency: 'USD', price: 9.99 },
GB: { currency: 'GBP', price: 7.99 },
DE: { currency: 'EUR', price: 8.99 },
JP: { currency: 'JPY', price: 1500 },
};
const plan = pricing[country] || { currency: 'USD', price: 9.99 };
res.json({ country, ...plan });
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Test it:
# Your own IP
curl http://localhost:3000/greeting
# Simulate a German visitor
curl -H "X-Forwarded-For: 85.214.132.117" http://localhost:3000/greeting
# {"country":"DE","message":"Hallo! Willkommen bei unserem Service."}
TypeScript Version
If you’re using TypeScript, here’s the typed version of the lookup client:
// ipinfo-client.ts
export interface IPInfoData {
ip: string;
country: string;
country_name: string;
region: string;
city: string;
org: string;
timezone: string;
latitude: number;
longitude: number;
}
const IPINFO_BASE = 'https://ipinfo.im/api/';
export async function lookupIP(ip?: string): Promise<IPInfoData | null> {
const url = ip ? `${IPINFO_BASE}?ip=${encodeURIComponent(ip)}` : IPINFO_BASE;
try {
const response = await fetch(url, {
headers: { Accept: 'application/json' },
signal: AbortSignal.timeout(8000),
});
if (!response.ok) return null;
return (await response.json()) as IPInfoData;
} catch {
return null;
}
}
Summary
Using IP geolocation in Node.js with ipinfo.im:
- No setup required — no API key, no npm package for the API itself
- Native
fetchworks in Node.js 18+ for zero-dependency integration - Use
axiosif you prefer its ergonomics and interceptor support - Always handle errors — network issues happen, and null-safe code is essential
- Cache lookups in production to avoid redundant API calls
- Add delays when doing batch lookups to stay respectful of the free API
Start building now — try the live API at ipinfo.im.