npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ipregistry/next

v1.0.1

Published

Official Ipregistry integration for Next.js: middleware-driven IP geolocation and threat detection for every request.

Readme

Ipregistry Next.js Library

License Actions Status npm

This is the official Next.js integration for the Ipregistry IP geolocation and threat data API. It is built on top of the official @ipregistry/client JavaScript SDK.

The middleware enriches every incoming request with Ipregistry data. That data is then available anywhere on the server with a single call: server components, route handlers, server actions, and getServerSideProps. Both the Edge and Node.js runtimes are supported, in the App Router and the Pages Router.

Request -> middleware (1 lookup, cached) -> x-ipregistry request header -> getIpregistry() anywhere

Features

  • One middleware, data everywhere: a single lookup per request, readable from any server context.
  • Built-in caching through the SDK's LRU cache, so repeated visits from the same IP do not consume additional credits.
  • Country blocking, threat/proxy/Tor blocking, and country redirects as composable one-line actions.
  • GDPR helper (isEuVisitor) based on the API's location.in_eu field.
  • Safe by default: fails open when Ipregistry is unreachable, skips static assets, strips spoofed headers, never calls the API from the browser, and never logs full IP addresses.
  • Trusted-proxy IP extraction presets for Vercel, Cloudflare, and Nginx, plus custom extractors.
  • TypeScript-first, with the official SDK's response types re-exported.

Getting started

You need an Ipregistry API key. Sign up at https://ipregistry.co to get one along with free lookups.

Requirements

  • Next.js 14 or newer
  • Node.js 20 or newer, or the Edge runtime

Installation

npm install @ipregistry/next

Setup in three steps

Step 1: configure your API key. Never expose it client-side, so only use IPREGISTRY_API_KEY and never a NEXT_PUBLIC_ variable:

# .env.local
IPREGISTRY_API_KEY=YOUR_API_KEY

Step 2: create the middleware:

// middleware.ts (next to your app/ directory)
import { createIpregistryMiddleware } from '@ipregistry/next/middleware'

export const middleware = createIpregistryMiddleware({
    fields: 'ip,location,security', // fetch only what you need
})

export const config = {
    matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Step 3: read the data anywhere on the server:

// app/page.tsx (server component)
import { getIpregistry } from '@ipregistry/next'

export default async function Page() {
    const { data } = await getIpregistry()

    return <h1>Hello {data?.location?.country?.name ?? 'visitor'}!</h1>
}
// app/api/geo/route.ts (route handler)
import { getIpregistry } from '@ipregistry/next'
import { NextResponse, type NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
    const { data } = await getIpregistry(request)

    return NextResponse.json({
        country: data?.location?.country?.code ?? null,
        city: data?.location?.city ?? null,
    })
}
// Pages Router (getServerSideProps)
import { getIpregistry } from '@ipregistry/next'
import type { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async ({ req }) => {
    const { data } = await getIpregistry(req)
    return { props: { country: data?.location?.country?.name ?? null } }
}

getIpregistry never throws and never triggers an API call. It returns an IpregistryContext:

interface IpregistryContext {
    ip: string | null       // the visitor IP the lookup ran for
    data: IpInfo | null     // the official SDK's IpInfo type
    skipped?: 'static-asset' | 'bot' | 'custom' | 'no-ip' | 'no-middleware'
    error?: { code?: string; message: string }
}

Configuration

Everything is optional. Explicit options take precedence over environment variables.

| Option | Environment variable | Default | Description | |---|---|---|---| | actions | None | [] | Decision hooks run after a successful lookup. | | apiKey | IPREGISTRY_API_KEY | None | Your Ipregistry API key (server-side only). | | baseUrl | IPREGISTRY_BASE_URL | default endpoint | API base URL; 'eu' selects the EU endpoint. | | cache | None | InMemoryCache | Any SDK IpregistryCache, or false to disable. | | client | None | None | A pre-configured IpregistryClient (advanced/testing). | | debug | None | false | Log skips and failures with anonymized IPs. | | developmentIp | None | None | Fixed IP used when the client IP is private (localhost). | | failClosed | None | false | Respond 503 (or a custom status) when the lookup fails. | | fields | IPREGISTRY_FIELDS | full response | Comma-separated field selection, e.g. 'ip,location,security'. | | ipSource | None | 'auto' | Where to read the client IP from (see below). | | maxRetries | None | 0 | Automatic retries. Off by default because retrying inside middleware would stall page loads. | | onError | None | None | Callback for lookup failures (monitoring). | | skip | None | None | Custom predicate to skip a request. | | skipBots | None | false | Skip crawlers: true (SDK heuristic) or a custom RegExp. | | skipStaticAssets | None | true | Skip /_next/*, favicon, and common asset extensions. | | timeout | IPREGISTRY_TIMEOUT | 3000 | Lookup timeout in milliseconds. |

Tip: set fields to fetch only what you use, keeping lookups fast and the request header small. 'ip,location,security' covers geo features, blocking, and GDPR detection.

The context travels from the middleware to your app as a compact request header: JSON, deflate-compressed when large, base64url-encoded. A full unfiltered payload stays around 1 KB. The middleware warns once if the encoded value ever grows past 6 KB so you can trim fields before hitting proxy header limits.

Country-based redirects

import {
    createIpregistryMiddleware,
    redirectByCountry,
} from '@ipregistry/next/middleware'

export const middleware = createIpregistryMiddleware({
    fields: 'ip,location',
    actions: [
        redirectByCountry({
            redirects: {
                FR: '/fr',                  // path on the same origin
                DE: 'https://example.de',   // or a country domain
            },
            preservePath: true, // /pricing -> /fr/pricing
        }),
    ],
})

Redirects are loop-safe: a visitor already under /fr (or already on example.de) is not redirected again. The default status is 307. Pass status: 308 for permanent redirects.

Blocking countries

import {
    blockCountries,
    createIpregistryMiddleware,
} from '@ipregistry/next/middleware'

export const middleware = createIpregistryMiddleware({
    fields: 'ip,location',
    actions: [
        blockCountries({ countries: ['KP', 'IR'] }), // 451 by default
    ],
})

Options: mode: 'allow' turns the list into an allowlist, unknown: 'block' also blocks visitors whose country could not be determined (the default is fail-open), and response lets you return a custom page.

Blocking proxies, Tor, and threats

import {
    blockThreats,
    createIpregistryMiddleware,
} from '@ipregistry/next/middleware'

export const middleware = createIpregistryMiddleware({
    fields: 'ip,security',
    actions: [
        // Blocks security.is_threat / is_attacker / is_abuser by default.
        // Anonymization signals are opt-in:
        blockThreats({ proxy: true, tor: true, vpn: true }),
    ],
})

Each flag maps to the same-named security.is_* field of the Ipregistry response. You can also make ad-hoc decisions with a custom action:

export const middleware = createIpregistryMiddleware({
    fields: 'ip,location,security',
    actions: [
        (context, request) =>
            context.data.security.is_tor &&
            request.nextUrl.pathname.startsWith('/checkout')
                ? new Response('Not available over Tor.', { status: 403 })
                : undefined, // undefined = continue
    ],
})

Actions run in order after a successful lookup, and the first one returning a Response wins. They never run when the lookup was skipped or failed (fail-open).

GDPR and EU detection

import { getIpregistry, isEuVisitor } from '@ipregistry/next'

export default async function Layout({ children }) {
    const context = await getIpregistry()

    return (
        <>
            {children}
            {isEuVisitor(context) && <CookieConsentBanner />}
        </>
    )
}

isEuVisitor uses the API's location.in_eu field. When the data is missing it returns false. Pass { assumeEu: true } to default to showing consent UIs instead:

isEuVisitor(context, { assumeEu: true })

Caching

Lookups are cached by default with the SDK's InMemoryCache (LRU, 2048 entries, 10-minute expiry), scoped to the runtime instance. Repeated requests from the same IP consume a single credit until expiry. Plug any store by implementing the SDK's IpregistryCache interface:

import type { IpregistryCache } from '@ipregistry/client'
import { InMemoryCache } from '@ipregistry/client'

// Bigger cache with a 1-hour expiry:
createIpregistryMiddleware({ cache: new InMemoryCache(16384, 3_600_000) })

// Or your own (Redis, Valkey, ...):
class MyCache implements IpregistryCache { /* get/put/invalidate/invalidateAll */ }
createIpregistryMiddleware({ cache: new MyCache() })

// Or disable caching entirely:
createIpregistryMiddleware({ cache: false })

IP extraction behind proxies

The visitor IP is read from proxy headers. Only trust headers your platform actually overwrites, otherwise clients can spoof their IP:

// Vercel
createIpregistryMiddleware({ ipSource: 'vercel' })

// Cloudflare (only trusts cf-connecting-ip)
createIpregistryMiddleware({ ipSource: 'cloudflare' })

// Nginx with `proxy_set_header X-Real-IP $remote_addr;`
createIpregistryMiddleware({ ipSource: 'nginx' })

// A single custom trusted header
createIpregistryMiddleware({ ipSource: { header: 'x-client-ip' } })

// Full control
createIpregistryMiddleware({
    ipSource: request => request.headers.get('x-my-edge-ip'),
})

The default ('auto') reads x-real-ip, then the first x-forwarded-for entry. This is correct on Vercel, Cloudflare, and any well-configured reverse proxy. Extracted values are validated; ports, IPv6 brackets, and zone IDs are stripped; private and reserved addresses are never sent to the API.

On localhost your IP is private, so no lookup happens. To exercise geo features in development:

createIpregistryMiddleware({
    developmentIp: process.env.NODE_ENV === 'development' ? '66.165.2.7' : undefined,
})

Saving credits on bots and static assets

Static assets (/_next/*, favicon, images, fonts, and similar) are always skipped by default. Search bots are skipped opt-in:

createIpregistryMiddleware({
    skipBots: true,               // SDK heuristic (bot, crawl, spider, slurp)
    // or a custom pattern:
    skipBots: /googlebot|bingbot|my-monitoring/i,
    // and any custom rule:
    skip: request => request.nextUrl.pathname.startsWith('/healthz'),
})

Your matcher in middleware.ts remains the primary filter. The built-in skips are a safety net.

Error handling

The middleware fails open by default. If Ipregistry is unreachable, the request times out, the API key is missing or invalid, or the response is malformed, the request continues normally with data: null and an error on the context. Users are never blocked by an outage, no exception ever escapes into your request pipeline, and full IP addresses are never logged.

const context = await getIpregistry()

if (context.error) {
    // e.g. { code: 'INVALID_API_KEY', message: '...' }
    // codes: Ipregistry API codes, plus MISSING_API_KEY / CLIENT_ERROR
}

For security-sensitive apps that must not serve traffic without IP intelligence, opt into fail-closed:

createIpregistryMiddleware({
    failClosed: true,        // 503 on lookup failure
    // failClosed: 403,      // or pick the status
    onError: error => reportToMonitoring(error),
})

Composing with existing middleware

// middleware.ts
import { createIpregistryMiddleware } from '@ipregistry/next/middleware'
import type { NextRequest } from 'next/server'

const withIpregistry = createIpregistryMiddleware({ fields: 'ip,location' })

export async function middleware(request: NextRequest) {
    const response = await withIpregistry(request)

    // Blocked or redirected by an action? Stop here.
    if (response.status !== 200 || response.headers.has('location')) {
        return response
    }

    // ... your own logic. Return `response` (not NextResponse.next())
    // so the Ipregistry request header keeps flowing to your app.
    return response
}

API reference

From @ipregistry/next:

| Export | Description | |---|---| | getIpregistry(source?) | Reads the request's Ipregistry context. Call without arguments in server components and server actions; pass the request or headers elsewhere. Never throws, never calls the API. | | isEuVisitor(input, options?) | Returns true when the visitor is in the European Union, based on location.in_eu. Accepts an IpInfo or an IpregistryContext. | | isThreat(input, options?) | Returns true when the IP is flagged by security.is_threat, is_attacker, or is_abuser. Proxy, Tor, VPN, and relay signals are opt-in through the options. | | isBot(input) | Returns true for bot user agents. Accepts a user agent string, a request, or a parsed SDK UserAgent. | | IPREGISTRY_HEADER | The request header name (x-ipregistry) used to carry the context. |

Types: IpregistryContext, IpregistryLookupContext, IpregistrySkipReason, IpregistryErrorInfo, ThreatOptions, plus the SDK's IpInfo, Location, Security, Connection, Company, Carrier, Currency, TimeZone, and UserAgent.

From @ipregistry/next/middleware (includes everything above, plus):

| Export | Description | |---|---| | createIpregistryMiddleware(config) | Creates the middleware that performs the lookup and attaches the context to the request. | | blockCountries(options) | Action that blocks (or exclusively allows) visitors by ISO 3166-1 country code. | | blockThreats(options) | Action that blocks visitors flagged by Ipregistry security data. | | redirectByCountry(options) | Action that redirects visitors to country-specific paths or domains, loop-safe. |

Types: IpregistryMiddlewareConfig, IpregistryAction, BlockCountriesOptions, BlockThreatsOptions, RedirectByCountryOptions, IpSource, IpExtractor, TrustedProxyPreset.

Examples

Complete minimal setups live in examples/app-router and examples/pages-router.

Migrating from @ipregistry/client

See MIGRATION.md. In short: keep the SDK for batch, ASN, and user-agent lookups and for background jobs; use this package to enrich request handling. It removes the client wiring, IP extraction, caching, and error-handling boilerplate from your Next.js code.

Other resources

License

Apache License 2.0. See LICENSE.txt.