Skip to content
InternetData

Node.js Express

The official Express middleware for the InternetData API.

See on GitHub

Getting Started

bash
npm install internetdata-express

Requires Node.js 22 or newer and Express 4.18 or newer. TypeScript types are included.

Without an API key a lookup is on Unlimited, the free plan: the country, the network and the company behind the address, Tor, relay, anycast, carrier and satellite space and the service flags, with no daily or monthly cap. Every lookup your app makes comes from your server's one address, though, so a busy site will see bursts refused. A paid plan answers every field, VPN, the proxy flags, hosting, CDN and the city among them; create a key in the console. A free account tries every field, 100 lookups a day.

js
import express from 'express';
import { internetdata } from 'internetdata-express';

const app = express();

app.set('trust proxy', 'loopback');  // your proxy; see "Where the client address comes from" below
app.use(internetdata({ apiKey: process.env.INTERNETDATA_API_KEY }));

app.get('/', (req, res) => {
    const { result } = req.internetdata;
    res.send(`Hello, visitor from ${result?.location.country_name ?? 'somewhere'}`);
});

By default nothing is blocked. Every request gets a req.internetdata and your own code decides what it means: a currency to show, a sign-up to review, a page to keep from some countries.

Blocking

Pass a blockCondition and a matching request is answered with 403 and never reaches your handlers.

js
app.use(internetdata({
    apiKey: process.env.INTERNETDATA_API_KEY,
    blockCondition: { location: { country: ['CU', 'IR', 'KP'] } },
}));

A condition is written in the shape of a result, and only the members you name are considered, so it can refuse on anything in the answer. The country works on every plan, and so do the first two of these:

js
blockCondition: { asn: { type: 'hosting' } }                          // the network's type
blockCondition: [{ isAnycast: true }, { isSatellite: true }]          // anycast and satellite space
blockCondition: [{ isHosting: true }, { isCdn: true }]                // traffic from servers, not people
blockCondition: [{ isVpn: true }, { isTor: true }, { isResproxy: true }]  // anonymity: a list is OR
blockCondition: { isVpn: true, vpn: { provider: 'nordvpn' } }         // one provider
blockCondition: { resproxy: { hits: { gte: 5 } } }                    // a numeric threshold

Values are matched by equality, strings without regard to case. An array means any-of. { gte, gt, lte, lt } compares numbers, and every bound you give must hold, so two of them are a range. Members you set to false or null are ignored, so a condition states the signals you act on; one that constrains nothing would match every request, and is refused when the middleware is created rather than silently blocking all your traffic.

Replace the refusal with onBlocked:

js
app.use(internetdata({
    apiKey: process.env.INTERNETDATA_API_KEY,
    blockCondition: { location: { country: ['CU', 'IR', 'KP'] } },
    onBlocked: (req, res) => res.status(451).render('unavailable'),
}));

Where the client address comes from

This is the setting that decides whether any of the above works, and it is the one thing only you can get right.

By default the middleware uses req.ip, which is Express's own accessor. Express resolves req.ip to the socket peer unless you set trust proxy. So if your app sits behind nginx, a load balancer, or a CDN and you have not set it, every visitor arrives wearing your proxy's address, and that is the address that gets looked up: every visitor gets your proxy's country and network, and if it is a hosting address, a hosting rule blocks everyone. nginx on the same machine is 127.0.0.1 and a load balancer usually has a private address, which the middleware answers locally without a lookup, as an address with no country, and logs a warning.

If you are behind a proxy you control, setting Express's own option to that proxy's address is the right fix and everything else here follows from it:

js
app.set('trust proxy', 'loopback');    // a proxy on the same machine
app.set('trust proxy', '10.0.0.0/8');  // or your load balancer's subnet

Name your proxies rather than setting true. With true, req.ip is the left-most X-Forwarded-For entry, and the visitor writes that one: nginx, AWS's Application Load Balancer and Cloudflare all append to the header rather than replace it, so a visitor who sends X-Forwarded-For: 1.1.1.1 is looked up as 1.1.1.1. A hop count such as 1 is no better, because it believes whoever connects to the app directly. Keep true only behind an edge you control that overwrites X-Forwarded-For.

For an edge that writes the address into its own header, name the header:

js
import { headerIpSelector } from 'internetdata-express';

app.use(internetdata({
    apiKey: process.env.INTERNETDATA_API_KEY,
    ipSelector: headerIpSelector('CF-Connecting-IP'),  // or True-Client-IP, or your own
}));

xffIpSelector() reads X-Forwarded-For directly. Be aware that the left-most entry is whatever the caller sent, because proxies append to that header, so it's only trustworthy when an edge you control overwrites it. If you know how many proxies sit in front, count from the right instead: xffIpSelector({ depth: 1 }) is the address your nearest proxy saw.

Anything else, pass your own function. It receives the Express request and returns an address:

js
ipSelector: (req) => req.headers['x-real-ip'] ?? req.ip,

If the address resolves to a private one, the middleware says so once on console.warn. That is expected on localhost and is the signal to fix your configuration anywhere else.

When a lookup fails

The request is let through, and the reason is recorded on req.internetdata.error. Our outage should not become yours, so a network failure, a rate limit or a rejected key all fail open.

js
app.get('/', (req, res) => {
    const { result, error } = req.internetdata;
    if (error) {
        req.log.warn({ kind: error.kind }, 'internetdata unavailable');
    }
    res.send(`Hello, visitor from ${result?.location.country_name ?? 'somewhere'}`);
});

Pass failClosed: true to block instead. Private addresses are answered locally and never fail, so this will not lock you out in development.

Cost and latency

Answers are cached per middleware for an hour, so a returning visitor costs nothing, and private addresses never leave the process. A cache miss is one request to our API, bounded at 2500 ms by default and not retried: on a request path, failing open quickly beats holding a visitor while we try again. Both are adjustable, along with the cache itself:

js
internetdata({ apiKey: KEY, timeoutMs: 1000, retries: 1, cache: { max: 50000, ttlMs: 600000 } })

Mount it on the routes that matter rather than the whole app, or skip what you do not care about:

js
app.use(internetdata({ apiKey: KEY, skip: (req) => req.path.startsWith('/static') }));

If you already hold an InternetData client, pass it as client and the middleware will share it rather than building a second cache.

Beyond a few million distinct visitors a day, stop calling the API per request: look addresses up locally in our databases instead.

Absent is not false

Unlimited, with a key or without one, leaves out isVpn, isHosting, isCdn and the four proxy flags, the detail behind them, and the paid members inside location, asn, company and abuse, such as location.city. A field your plan does not include is undefined, which means "not in your plan" rather than "checked, and no".

js
req.internetdata.result?.isVpn ?? false   // when you only want the flag

A blockCondition naming a member your plan does not serve, location.city on Unlimited included, can never match, so the middleware warns once instead of failing silently. Set onMissingField: 'throw' to make it an error.