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
- The cache is per process and is lost on restart. At 50,000 lookups a month, caching decides whether a busy site stays inside the free quota.
- Only look up addresses you have validated (
isIPabove). A429or timeout returnsnull, so decide on a default. - Do not log full visitor addresses together with locations unless you need to; see our privacy page for what we store.
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.