DEV Community

Vix
Vix

Posted on

Getting the public IP in Node.js and Express — no API key, no rate limits, no hassle

Getting the public IP in Node.js and Express — no API key, no rate limits, no hassle

Node.js is one of the most popular runtimes for building APIs, backend services, and CLI tools that need to know about network context. This article covers how to get the public IP address — your own server's or your client's — across the most common Node.js scenarios, using IPPubblico.org: free, no key required, HTTPS, CORS enabled.


Use case 1 — Your server's own public IP (script / CLI)

The simplest case: a Node.js script that needs to know its own public IP. Useful in deployment scripts, diagnostics, DDNS updaters, or any automation that needs to report its own address.

With fetch (Node 18+):

async function getPublicIP() {
  try {
    const res = await fetch('https://ipv4.ippubblico.org/', { signal: AbortSignal.timeout(5000) });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return (await res.text()).trim();
  } catch (err) {
    console.error('Failed to get public IP:', err.message);
    return null;
  }
}

const ip = await getPublicIP();
console.log('Public IP:', ip); // 203.0.113.42
Enter fullscreen mode Exit fullscreen mode

With axios:

import axios from 'axios';

async function getPublicIP() {
  try {
    const { data } = await axios.get('https://ipv4.ippubblico.org/', { timeout: 5000 });
    return data.trim();
  } catch (err) {
    console.error('Failed to get public IP:', err.message);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

With node-fetch (older Node versions):

import fetch from 'node-fetch';

async function getPublicIP() {
  const res = await fetch('https://ipv4.ippubblico.org/');
  return (await res.text()).trim();
}
Enter fullscreen mode Exit fullscreen mode

Use case 2 — Full geolocation data

When you need country, city, ISP and timezone in addition to the IP:

async function getIPInfo() {
  const res = await fetch('https://ippubblico.org/?api=1', {
    signal: AbortSignal.timeout(5000)
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();

  return {
    ip:          data.ip,
    country:     data.geo?.country,
    countryCode: data.geo?.country_code,
    city:        data.geo?.city,
    region:      data.geo?.region,
    isp:         data.isp,
    timezone:    data.timezone,
    lat:         data.geo?.lat,
    lon:         data.geo?.lon,
  };
}

const info = await getIPInfo();
console.log(info);
// {
//   ip: '203.0.113.42',
//   country: 'Italy',
//   countryCode: 'IT',
//   city: 'Milan',
//   region: 'Lombardy',
//   isp: 'Telecom Italia',
//   timezone: 'Europe/Rome',
//   lat: 45.4654,
//   lon: 9.1859
// }
Enter fullscreen mode Exit fullscreen mode

Use case 3 — Express middleware for client IP geolocation

The most common Express scenario: detecting where your users are connecting from. This middleware resolves the client's IP using IPPubblico and attaches the result to req.ipInfo for use in any route.

import express from 'express';

const app = express();

// Simple in-memory cache to avoid hitting the API on every request
const geoCache = new Map();
const CACHE_TTL = 60 * 60 * 1000; // 1 hour

async function resolveIP(ip) {
  const cached = geoCache.get(ip);
  if (cached && Date.now() - cached.ts < CACHE_TTL) {
    return cached.data;
  }

  try {
    const res = await fetch(`https://ippubblico.org/?api=1`, {
      signal: AbortSignal.timeout(3000)
    });
    const data = await res.json();
    const result = {
      ip:          data.ip,
      country:     data.geo?.country,
      countryCode: data.geo?.country_code,
      city:        data.geo?.city,
      isp:         data.isp,
      timezone:    data.timezone,
    };
    geoCache.set(ip, { data: result, ts: Date.now() });
    return result;
  } catch {
    return null;
  }
}

// Middleware
app.use(async (req, res, next) => {
  const forwarded = req.headers['x-forwarded-for'];
  const clientIP  = forwarded ? forwarded.split(',')[0].trim() : req.socket.remoteAddress;
  req.clientIP  = clientIP;
  req.ipInfo    = await resolveIP(clientIP);
  next();
});

// Use in any route
app.get('/hello', (req, res) => {
  const country = req.ipInfo?.country ?? 'Unknown';
  res.json({
    message: `Hello from ${country}!`,
    ip:      req.clientIP,
    info:    req.ipInfo,
  });
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Use case 4 — Country-based access control

Restricting or customising content based on country — without a paid API key:

const BLOCKED_COUNTRIES = ['XX', 'YY']; // ISO country codes
const PREMIUM_COUNTRIES  = ['US', 'GB', 'DE', 'FR', 'IT'];

app.use(async (req, res, next) => {
  const forwarded = req.headers['x-forwarded-for'];
  const ip = forwarded ? forwarded.split(',')[0].trim() : req.socket.remoteAddress;

  try {
    const geoRes = await fetch('https://ippubblico.org/?api=1', {
      signal: AbortSignal.timeout(3000)
    });
    const data = await geoRes.json();
    req.countryCode = data.geo?.country_code ?? null;
  } catch {
    req.countryCode = null;
  }
  next();
});

app.get('/content', (req, res) => {
  if (BLOCKED_COUNTRIES.includes(req.countryCode)) {
    return res.status(403).json({ error: 'Not available in your region' });
  }

  const isPremiumRegion = PREMIUM_COUNTRIES.includes(req.countryCode);
  res.json({
    content:  'Here is your content',
    tier:     isPremiumRegion ? 'premium' : 'standard',
    currency: req.countryCode === 'GB' ? 'GBP' : 'EUR',
  });
});
Enter fullscreen mode Exit fullscreen mode

Use case 5 — DDNS updater script

A complete Node.js DDNS updater that checks the public IP every 5 minutes and logs when it changes:

import fs   from 'fs/promises';
import path from 'path';

const CACHE_FILE  = '/tmp/last_known_ip.txt';
const CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes

async function getPublicIP() {
  const res = await fetch('https://ipv4.ippubblico.org/', {
    signal: AbortSignal.timeout(5000)
  });
  return (await res.text()).trim();
}

async function getLastKnownIP() {
  try {
    return (await fs.readFile(CACHE_FILE, 'utf-8')).trim();
  } catch {
    return null;
  }
}

async function saveIP(ip) {
  await fs.writeFile(CACHE_FILE, ip, 'utf-8');
}

async function updateDDNS(newIP) {
  // Add your DDNS provider API call here
  console.log(`[DDNS] Updating record to ${newIP}`);
}

async function check() {
  try {
    const currentIP  = await getPublicIP();
    const lastKnownIP = await getLastKnownIP();

    if (currentIP !== lastKnownIP) {
      console.log(`[IP changed] ${lastKnownIP ?? 'unknown'}${currentIP}`);
      await updateDDNS(currentIP);
      await saveIP(currentIP);
    } else {
      console.log(`[IP unchanged] ${currentIP}`);
    }
  } catch (err) {
    console.error('[Error]', err.message);
  }
}

// Run immediately then every 5 minutes
check();
setInterval(check, CHECK_INTERVAL);
Enter fullscreen mode Exit fullscreen mode

Use case 6 — IPv4 and IPv6 detection

When you need to know both protocols:

async function getBothIPs() {
  const res = await fetch('https://ippubblico.org/?text=1', {
    signal: AbortSignal.timeout(5000)
  });
  const text = await res.text();
  const lines = text.trim().split('\n');

  const parse = (line, prefix) => {
    const val = line.replace(prefix, '').trim();
    return val === 'NONE' ? null : val;
  };

  return {
    ipv4: parse(lines[0], 'IPv4:'),
    ipv6: parse(lines[1], 'IPv6:'),
  };
}

const ips = await getBothIPs();
console.log(ips);
// { ipv4: '203.0.113.42', ipv6: null }
// or
// { ipv4: '203.0.113.42', ipv6: '2001:db8::1' }
Enter fullscreen mode Exit fullscreen mode

Handling rate limits

If your application makes many requests from the same IP in a short period, the API responds with 429 Too Many Requests and a Retry-After header. A robust fetch wrapper:

async function fetchWithRetry(url, maxRetries = 2) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch(url, { signal: AbortSignal.timeout(5000) });

    if (res.status === 429) {
      const retryAfter = parseInt(res.headers.get('Retry-After') ?? '20', 10);
      console.warn(`Rate limited. Waiting ${retryAfter}s...`);
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      continue;
    }

    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res;
  }
  throw new Error('Max retries exceeded');
}

// Usage
const res  = await fetchWithRetry('https://ippubblico.org/?api=1');
const data = await res.json();
Enter fullscreen mode Exit fullscreen mode

In practice, caching results (as shown in the Express middleware above) eliminates most rate limit concerns.


Quick reference

Need Endpoint Response
IPv4 only https://ipv4.ippubblico.org/ 203.0.113.42
IPv6 only https://ipv6.ippubblico.org/ 2001:db8::1 or NONE
Both protocols https://ippubblico.org/?text=1 IPv4: x\nIPv6: x
Full geolocation https://ippubblico.org/?api=1 JSON with country, city, ISP

Full API documentation: ippubblico.org/docs.html


Conclusion

IPPubblico covers Node.js use cases from a one-liner in a CLI script to a full Express middleware with caching and retry logic. The native fetch API available since Node 18 makes the integration particularly clean — no extra dependencies for the basic case.

The DDNS updater pattern is worth bookmarking: it's a complete, production-ready script that works on any server or VPS with Node.js installed, handling IP change detection, persistence, and error recovery in under 50 lines.


Using this in a Node.js project? Share what you built in the comments.

Top comments (0)