← Docs and guides

Redirect or set the locale by country in Next.js middleware

Look up the visitor's country once, remember it in a cookie, and send them to the right locale. The key stays on the server.

Before you start

Create a free key on the home page and store it as the environment variable IPATLAS_KEY (server-side only: no NEXT_PUBLIC_ prefix). Server-side requests share your key's quota of 50,000 per month, so the code below caches lookups and skips visitors who already have a cookie.

middleware.ts

import { NextResponse, type NextRequest } from "next/server";

// country code -> locale path; everything else gets the default
const LOCALES: Record<string, string> = { DE: "de", AT: "de", CH: "de", FR: "fr", ES: "es" };

const cache = new Map<string, { cc: string | null; exp: number }>();

async function countryOf(ip: string): Promise<string | null> {
  const hit = cache.get(ip);
  if (hit && hit.exp > Date.now()) return hit.cc;

  let cc: string | null = null;
  let ttl = 60_000;                                   // cache failures briefly
  try {
    const r = await fetch(`https://api.ipatlas.dev/v1/${encodeURIComponent(ip)}`, {
      headers: process.env.IPATLAS_KEY ? { "X-API-Key": process.env.IPATLAS_KEY } : {},
      signal: AbortSignal.timeout(1000),              // never hold up page loads
    });
    if (r.ok) {
      cc = (await r.json()).country_code ?? null;
      ttl = 6 * 3600_000;
    }
  } catch {}
  if (cache.size >= 5000) cache.clear();
  cache.set(ip, { cc, exp: Date.now() + ttl });
  return cc;
}

export async function middleware(req: NextRequest) {
  if (req.cookies.has("locale")) return NextResponse.next();   // decided before: no lookup

  // Use the client address your platform sets. Behind a proxy this is the first x-forwarded-for entry.
  const ip = req.headers.get("x-forwarded-for")?.split(",")[0].trim();
  const cc = ip ? await countryOf(ip) : null;
  const locale = (cc && LOCALES[cc]) || "en";

  const res = locale === "en"
    ? NextResponse.next()
    : NextResponse.redirect(new URL(`/${locale}`, req.url));
  if (cc) res.cookies.set("locale", locale, { maxAge: 60 * 60 * 24 * 30, path: "/" });   // not after a failed lookup
  return res;
}

export const config = { matcher: "/" };               // only the home page

How it behaves

Limits: without a key, 1,000 requests per day per IP address (10 req/s); with a free key, 50,000 requests per month (50 req/s). Over a limit the API returns 429. IP geolocation is approximate: country is usually right, city often is not.

Attribution

The data is DB-IP Lite, licensed CC BY 4.0. If you show or use it in a public product, link to DB-IP, for example in your footer:

<a href="https://db-ip.com">IP Geolocation by DB-IP</a>

See the API docs for fields, limits and error codes.