Integrations · Next.js

IPAPI.GR for Next.js

One proxy.ts file looks up the visitor's IP before your routes render, hands the country and timezone to Server Components as request headers, and refuses self-declared AI crawlers whose address is not a known AI crawler.

Written for
Next.js 16, App Router
Runs in
proxy.ts (Node.js runtime)
Client IP from
x-real-ip / x-forwarded-for

01 · Setup

3 steps, copy and paste.

Every lookup runs on your server or edge, so the key never reaches a browser. Each example times out quickly, caches answers per IP and lets the visitor through if the lookup fails.

  1. Store the key server-side

    Put the key in .env.local. Without the NEXT_PUBLIC_ prefix it never reaches the browser bundle.

    .env.localShell
    # .env.local (server-only: never prefix it with NEXT_PUBLIC_)
    IPAPI_KEY=your_api_key_here
  2. Add proxy.ts

    In Next.js 16 the middleware file is called proxy.ts and runs on the Node.js runtime. It skips the lookup when the visitor already has a country cookie, but always checks requests that claim to be an AI crawler.

    proxy.tsTypeScript
    // proxy.ts (Next.js 16+; called middleware.ts before v16). Runs before every matched route.
    import { NextResponse } from 'next/server';
    import type { NextRequest } from 'next/server';
    
    const LOOKUP = 'https://ipapi.gr/api/lookup/';
    const AI_CLAIM = /GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User/i;
    
    type Lookup = {
      country_code?: string;
      timezone?: string;
      risk?: { is_ai: boolean; is_crawler: boolean; is_scanner: boolean }; // paid plans only
    };
    
    // Missing values come back as the string "No Data".
    const known = (v?: string) => (v && v !== 'No Data' ? v : '');
    
    // On Vercel the platform sets these headers. Self-hosted, make sure your reverse proxy overwrites them.
    function clientIp(request: NextRequest): string | null {
      return request.headers.get('x-real-ip') ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? null;
    }
    
    async function lookup(ip: string): Promise<Lookup | null> {
      try {
        const res = await fetch(`${LOOKUP}${encodeURIComponent(ip)}?key=${encodeURIComponent(process.env.IPAPI_KEY ?? '')}`, {
          signal: AbortSignal.timeout(1500),
        });
        return res.ok ? ((await res.json()) as Lookup) : null;
      } catch {
        return null; // timeout or network error: fail open
      }
    }
    
    export async function proxy(request: NextRequest) {
      const ip = clientIp(request);
      const claimsAi = AI_CLAIM.test(request.headers.get('user-agent') ?? '');
      const saved = request.cookies.get('visitor_country')?.value;
    
      // Visitors keep their country in a cookie, so only the first request costs a lookup.
      // Fine for localisation; a cookie is easy to forge, so bot claims are always checked by IP.
      if (!ip || (saved && !claimsAi)) {
        const headers = new Headers(request.headers);
        headers.set('x-visitor-country', saved ?? '');
        return NextResponse.next({ request: { headers } });
      }
    
      const data = await lookup(ip);
      if (data?.risk && claimsAi && !data.risk.is_ai) {
        return new NextResponse('Unverified crawler', { status: 403 });
      }
    
      const country = known(data?.country_code);
      const headers = new Headers(request.headers);
      headers.set('x-visitor-country', country);
      headers.set('x-visitor-timezone', known(data?.timezone));
      const response = NextResponse.next({ request: { headers } });
      if (country) response.cookies.set('visitor_country', country, { maxAge: 60 * 60 * 24, sameSite: 'lax' });
      return response;
    }
    
    export const config = {
      // Skip static files, images and API routes.
      matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|svg|webp)$).*)'],
    };
  3. Read it in a Server Component

    headers() is async since Next.js 15. Reading it makes the route dynamic, which is what you want for per-visitor content.

    app/page.tsxTSX
    // app/page.tsx: read what proxy.ts set, in any Server Component.
    import { headers } from 'next/headers';
    
    export default async function Page() {
      const h = await headers();
      const country = h.get('x-visitor-country') || null; // e.g. "GR", or null when unknown
      return <p>{country === 'GR' ? 'Γεια σας!' : 'Hello!'}</p>;
    }

02 · The response

What you get back.

Free keys return location and network data: country, country_code, region, city, zipcode, latitude, longitude, timezone, isp, organization and asn.

Keys on any paid plan add risk.is_ai, risk.is_crawler and risk.is_scanner, which tell you when an address belongs to a known AI crawler, search crawler or internet scanner. The examples use is_ai to refuse requests whose user agent claims an AI crawler while the address isn't one. false means "not a known bot", not "human".

03 · Pitfalls

What usually goes wrong.

Trust the right header
On Vercel, x-real-ip and x-forwarded-for are set by the platform. Self-hosted, make sure your reverse proxy overwrites them; otherwise a visitor can send any address.
Still on Next.js 15?
Name the file middleware.ts and export a function called middleware. The body works unchanged.
Don't call the API from the browser
A client component would expose your key. Keep lookups in proxy.ts, Route Handlers or Server Components.
Website restrictions block server calls
A key restricted to "Allowed websites" needs an Origin or Referer header, which server-side code does not send, so the call is refused. For a server key, leave websites empty and restrict by your server's outgoing IP address instead.
"No Data" is a string, not null
When no value is known for a field, the field is still present and set to "No Data". Every snippet converts it to null before you use it.

04 · Questions

Good to know.

More in the docs and the FAQ.

Does this work on Vercel and self-hosted Next.js?

Yes. The proxy only needs fetch and the client IP header. On Vercel the platform sets that header; on your own server, your reverse proxy must.

Why a cookie instead of a cache?

The Next.js docs advise against relying on shared globals in proxy, so the snippet stores the country in a cookie: one lookup per visitor per day. Bot claims skip the cookie because cookies are easy to forge.

Do I need a paid plan?

No. Country, timezone, city, ISP and ASN work with a free key. The AI-crawler check needs the risk flags, which come with every paid plan; on a free key that block is simply skipped.

Ship it today.

Free: 1.000 requests a day for location and network data. Bot flags and higher limits on every paid plan, from 9,00 € a month (VAT included).