← Docs and guides

IP geolocation middleware for Node.js and Express

A small Express middleware that puts the visitor's country, city and timezone on req.geo, with a cache, a timeout and no extra dependencies.

1. The middleware (Node 18+, Express 4 or 5)

Optionally create a free key on the home page and export it as IPATLAS_KEY; keep it on the server. Save this as ipatlas.js. It validates the address, caches answers for six hours, lets parallel requests from one visitor share a single lookup, and gives up after one second, so req.geo is null instead of a slow or failed request:

const { isIP } = require("node:net");

function ipatlas({
  key = process.env.IPATLAS_KEY,   // optional; keyless is limited to 1,000 requests/day/IP
  ttl = 6 * 3600 * 1000,           // successful answers: 6 hours
  max = 10000,                     // bound the memory use
  timeout = 1000,                  // never hold up your own requests
} = {}) {
  const cache = new Map();         // ip -> { geo, exp }
  const pending = new Map();       // ip -> Promise, so parallel requests share one lookup

  function lookup(ip) {
    const hit = cache.get(ip);
    if (hit && hit.exp > Date.now()) return hit.geo;
    if (pending.has(ip)) return pending.get(ip);

    const p = (async () => {
      let geo = null, life = 60 * 1000;            // failures: remember for a minute
      try {
        const r = await fetch(`https://api.ipatlas.dev/v1/${encodeURIComponent(ip)}`, {
          headers: key ? { "X-API-Key": key } : {},
          signal: AbortSignal.timeout(timeout),
        });
        if (r.ok) {                                // a 429 or 401 counts as failed
          const d = await r.json();
          if (d.country_code) {
            geo = { country: d.country_code, countryName: d.country, city: d.city ?? null, timezone: d.timezone ?? null };
            life = ttl;
          }
        }
      } catch {}                                   // timeout or network error: geo stays null
      if (cache.size >= max) cache.delete(cache.keys().next().value);
      cache.set(ip, { geo, exp: Date.now() + life });
      return geo;
    })().finally(() => pending.delete(ip));

    pending.set(ip, p);
    return p;
  }

  return async (req, res, next) => {
    const ip = (req.ip || "").replace(/^::ffff:/i, "");   // IPv4-mapped IPv6 form
    req.geo = isIP(ip) ? await lookup(ip) : null;          // { country: "DE", ... } or null
    next();
  };
}

// Optional: refuse some countries. An unknown location (null) is let through.
const blockCountries = (...codes) => (req, res, next) =>
  req.geo && codes.includes(req.geo.country) ? res.status(403).send("Not available in your country") : next();

module.exports = { ipatlas, blockCountries };

2. Use it

const express = require("express");
const { ipatlas, blockCountries } = require("./ipatlas");

const app = express();
app.set("trust proxy", 1);                     // one proxy in front of you; use false if none, or req.ip can be spoofed
app.use(ipatlas());

app.get("/", (req, res) => {
  const g = req.geo;
  res.send(g ? `Hello from ${g.city ?? g.countryName} (${g.country})` : "Hello from somewhere");
});
app.get("/shop", blockCountries("XX"), (req, res) => res.send("Shop"));   // replace XX with ISO country codes

app.listen(process.env.PORT || 3000);

Request / with an X-Forwarded-For: 8.8.8.8 header (behind a proxy that sets it) to see Hello from Mountain View (US). Without a public address req.geo is null.

Notes

Limits: without a key, 1,000 requests per day per IP address (10 req/s); with a free key, 50,000 requests per month (50 req/s). Over a limit the API returns 429. IP geolocation is approximate: country is usually right, city often is not.

Attribution

The data is DB-IP Lite, licensed CC BY 4.0. If you show or use it in a public product, link to DB-IP, for example in your footer:

<a href="https://db-ip.com">IP Geolocation by DB-IP</a>

See the API docs for fields, limits and error codes.