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.
-
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
-
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)$).*)'], }; -
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".
All response fields · AI crawlers we recognise · Bot signal coverage
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.
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.
From the blog
The concepts behind it.
- Get a visitor's country from their IP address in Node.js, Python, PHP and Go
- X-Forwarded-For spoofing: the security risk and the trusted-proxy model
- Redirect visitors by country without hurting SEO or locking out search engines
- Localize currency and shipping country by IP: show local prices without trapping customers
Other integrations
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).