IP Geolocation in Next.js: Browser and Server Patterns
Next.js can render the same page on the server or in the browser, and that choice changes which Watch-IP endpoint actually resolves a visitor's location — or resolves your server's instead.
Before you start
- A publishable visitor key for the client pattern, or a secret lookup key for the server pattern — see authentication.
- A Next.js App Router project (examples assume Next.js 16's Server/Client Component split).
- For the server pattern, a header your hosting platform sets on incoming requests with the visitor's real IP (for example CF-Connecting-IP on Cloudflare) — not one a client could set itself.
1. Understand what each pattern actually calls
GET /v1/geo identifies whoever's connection reaches the API — a visitor's browser calling it directly, or your Next.js server if your own code calls it during server rendering. Calling GET /v1/geo from a Server Component or Route Handler resolves your server's network location, not the visitor's, which is usually not what you want. Two patterns actually resolve the visitor: run the request in the browser (Client Component), or run it on your server against a supplied IP with GET /v1/lookup/{ip}.
2. Client Component: call GET /v1/geo from the browser
This is the same request the JavaScript and React guides use — a Client Component that fetches with a publishable key. The browser's own Origin header satisfies the key's allowed-origins check, so your app's public URL needs to be on that list.
TypeScript (Client Component)
"use client";
import { useEffect, useState } from "react";
export function VisitorLocation() {
const [city, setCity] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
fetch("https://api.watch-ip.com/v1/geo", {
headers: { "X-Api-Key": "YOUR_PUBLISHABLE_KEY" },
signal: controller.signal,
})
.then((res) => (res.ok ? res.json() : null))
.then((geo) => setCity(geo?.city ?? null))
.catch(() => {});
return () => controller.abort();
}, []);
return <span>{city ?? "your area"}</span>;
}3. Server Component or Route Handler: look up the visitor's IP explicitly
To resolve a visitor's location during server rendering, don't call GET /v1/geo — call GET /v1/lookup/{ip} with a secret key and the visitor's real IP, read from whichever header your hosting platform sets on the incoming request. Never trust a plain X-Forwarded-For value a client could set itself unless your platform strips and re-sets it at the edge.
TypeScript (Server Component)
import { headers } from "next/headers";
export async function VisitorLocationServer() {
const ip = (await headers()).get("cf-connecting-ip");
if (!ip) return null;
const response = await fetch(`https://api.watch-ip.com/v1/lookup/${ip}`, {
headers: { Authorization: "Bearer YOUR_SECRET_KEY" },
cache: "no-store",
});
if (!response.ok) return null;
const geo = await response.json();
return <span>{geo.city ?? "your area"}</span>;
}4. Don't cache one visitor's result for another
cache: 'no-store' above is deliberate: a lookup result is specific to the visitor who requested it. If this component renders inside a route Next.js or a CDN can cache — a static page, an ISR page, a shared fetch cache — a cached response would show one visitor's city to everyone after it. Keep the personalized branch dynamically rendered, and never fold a lookup response into a page-level or CDN-level cache.
Errors and edge cases
A missing or blank IP header (some local or proxy setups don't set one) means there's no visitor IP to look up — render the fallback rather than calling the endpoint with an empty string. A 403 from GET /v1/lookup/{ip} usually means a publishable key was used where the route needs a secret key; a 400 means the header held something that isn't a valid IPv4/IPv6 address. See errors and rate limits.
Next step
For a component you can reuse across client-only React apps as well as Next.js, see the React guide — the Client Component pattern above is a Next.js-specific wrapper around the same request.