← Docs and guides

Server-side country lookup with caching (Express or nginx)

Do the lookup on your server so the API key stays private, and cache it so repeat visitors cost nothing.

1. A cached lookup helper (Node 18+)

Create a free key on the home page and export it as IPATLAS_KEY. Save this as geo.js:

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

const KEY = process.env.IPATLAS_KEY;                 // keep it on the server
const TTL = 6 * 3600 * 1000;                         // positive answers: 6 hours
const MAX = 10000;                                   // bound the memory use
const cache = new Map();

async function countryOf(ip) {
  if (!isIP(ip)) return null;
  const hit = cache.get(ip);
  if (hit && hit.exp > Date.now()) return hit.cc;

  let cc = null, ttl = 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(1000),             // never hold up your own requests
    });
    if (r.ok) { cc = (await r.json()).country_code ?? null; ttl = TTL; }
  } catch {}

  if (cache.size >= MAX) cache.delete(cache.keys().next().value);   // drop the oldest entry
  cache.set(ip, { cc, exp: Date.now() + ttl });
  return cc;
}

module.exports = { countryOf };

2a. Express

const express = require("express");
const { countryOf } = require("./geo");

const app = express();
app.set("trust proxy", 1);                           // number of proxies in front of you, so req.ip is the visitor

app.use(async (req, res, next) => {
  req.country = await countryOf(req.ip);             // "DE", or null if unknown
  next();
});

app.get("/", (req, res) => res.send(`Hello from ${req.country ?? "somewhere"}`));
app.listen(3000);

2b. nginx

nginx cannot call an HTTP API by itself, so use the standard auth_request pattern: nginx asks a tiny local service for the country and forwards it to your app as a header. First the service, saved as country-service.js next to geo.js. It always answers 204, so a failed lookup never blocks a request:

const http = require("node:http");
const { countryOf } = require("./geo");

http.createServer(async (req, res) => {
  const cc = await countryOf(req.headers["x-real-ip"]);
  res.writeHead(204, { "X-Country": cc ?? "" }).end();
}).listen(3001, "127.0.0.1");

Then the nginx configuration (needs ngx_http_auth_request_module, which is in the official packages; check with nginx -V):

location = /_country {
    internal;
    proxy_pass http://127.0.0.1:3001;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Real-IP $remote_addr;         # the address to look up
}

location / {
    auth_request /_country;
    auth_request_set $country $upstream_http_x_country;
    proxy_set_header X-Country $country;             # overwrites anything the client sent
    proxy_pass http://127.0.0.1:3000;
}

If another proxy or CDN sits in front of nginx, use the real client address there (for example with set_real_ip_from and the realip module) before it reaches $remote_addr. Your app now reads X-Country on every request.

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.