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
trust proxydecides wherereq.ipcomes from. Set it to the real number of proxies in front of your app; too high lets visitors spoof their address throughX-Forwarded-For.- Blocking by country is easy to bypass with a VPN and the data is approximate, so use it for convenience (language, currency, content) rather than as a security control. The
blockCountrieshelper lets unknown locations through; flip that if you need the opposite. - The cache is per process. For several instances or a shared cache, see the caching and nginx guide.
- 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.