Watch-IP

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.

Related pages