# Check if an IP address is valid in Node.js: stdlib, regex, and classification

_2026-07-18 · Boundstone (https://boundstone.io/blog/validate-ip-nodejs)_


You've got a string that's supposed to be an IP address — from a form, a log line, an `X-Forwarded-For` header — and you need to know if it's real. In Node.js this is one of the rare validation problems the standard library already solves cleanly, so the main thing is to *not* reach for a regex. Here's the right way, and where the interesting questions start once the format check passes.

## Skip the regex

The IPv4 regex you'll find looks reasonable and is quietly wrong:

```javascript
const IPV4 = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
IPV4.test("999.999.999.999"); // true — but that's not a valid IP
```

Each octet has to be 0–255, which the pattern above doesn't enforce, so you end up bolting on range checks. And the IPv6 equivalent — with its `::` zero-compression, embedded IPv4, and zone IDs — is a genuine monster that almost nobody gets right. Don't write it.

## Use `net.isIP`

Node's built-in `net` module parses IP addresses properly, IPv4 and IPv6, with no dependency:

```javascript
import net from "node:net";

net.isIP("8.8.8.8");                       // 4
net.isIP("2606:4700:4700::1111");          // 6
net.isIP("999.999.999.999");               // 0  (not valid)
net.isIP("not-an-ip");                     // 0

net.isIPv4("10.0.0.1");                    // true
net.isIPv6("::1");                         // true
```

`net.isIP` returns `0`, `4`, or `6` — falsy when invalid, and the version number when valid. That's the whole format check:

```javascript
function isValidIp(value) {
  return net.isIP(value) !== 0;
}
```

For validating that a string is a well-formed IP address, that's the answer. No library, no regex, no edge cases you forgot. If you only needed the format check, you can stop reading here — genuinely.

## "Valid" is not "trustworthy"

Here's where it gets interesting. `net.isIP` tells you `10.0.0.1` is a perfectly valid IPv4 address. It does **not** tell you that `10.0.0.1` is a private address that should never appear as the source of a public request — which, if you're reading it out of a signup or an `X-Forwarded-For` header, is a red flag worth acting on.

That's a *classification* question, and the standard library doesn't answer it. The categories that matter:

- **Private** (`10.x`, `172.16–31.x`, `192.168.x`) — only valid inside a local network.
- **Loopback** (`127.x`, `::1`) — the machine talking to itself.
- **Link-local, reserved, documentation** — ranges that shouldn't be a real user's public source.
- **Bogon** — an address from unallocated or reserved space appearing where a routable one should be; a classic spoofing tell.

You can implement these range checks yourself — it's arithmetic on the address bytes — or get them from an API that already has them. Boundstone's IP endpoint returns the classification alongside the format check, and it's pure arithmetic too, so it's part of the free tier:

```javascript
const res = await fetch("https://api.boundstone.io/v1/verify/ip", {
  method: "POST",
  headers: { authorization: "Bearer bs_live_YOUR_KEY", "content-type": "application/json" },
  body: JSON.stringify({ ip: "10.0.0.1" }),
});
const data = await res.json();

console.log(data.valid, data.version, data.classification, data.is_public, data.is_bogon);
// true 4 "private" false true
```

## The honest boundary

Classification is where our IP endpoint stops today, and the response says so out loud:

```javascript
console.log("checked:    ", data.checks.performed);
// ['format', 'version', 'range_classification']

console.log("not checked:", data.checks.not_performed);
// ['geolocation', 'asn', 'hosting_datacenter', 'proxy_vpn_tor', 'reputation']
```

Everything on that second list — where the IP is, which network owns it, whether it's a datacenter, whether it's a VPN or Tor exit — needs licensed data we haven't shipped. So it's marked `not_performed` rather than approximated. That's a deliberate line: validating and classifying an address is honest, cheap, and useful; guessing its geolocation and calling it a fact is not. When the licensed intelligence layer ships, it'll appear in `performed` the day it's real, and not a day before.

You can try the classification with no key on the [IP validator](/tools/ip-validator).

## The short version

To check whether a string is a valid IP in Node.js, use `net.isIP` — it's built in, correct, and handles IPv4 and IPv6. That answers *format*. If your real question is "should I trust an address coming from this source," you want *classification* — public vs. private vs. bogon — which the stdlib doesn't do but you can compute or call for. And treat anything past that (geolocation, VPN detection) as a separate, heavier problem that any honest tool will tell you when it hasn't actually solved.
