Type something to search...
Next.js userAgent

Next.js userAgent

Every incoming request carries a User-Agent header — a single, dense string like Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15... that in theory tells you the browser, device, and OS making the request. In practice, parsing that string yourself is miserable: user agent strings are inconsistent across vendors, full of historical baggage (yes, most desktop browsers still claim to be "Mozilla/5.0" for compatibility reasons dating back decades), and easy to get subtly wrong.

Next.js ships a small helper for exactly this problem: userAgent(). It takes a request object and hands you back a structured, already-parsed object — browser name and version, device type, OS, rendering engine, CPU architecture, and a bot flag — instead of a string you'd otherwise have to regex your way through.

This article is the focused reference for userAgent() itself: its full return shape, where you're expected to call it, and the practical patterns and gotchas that come up once you actually start branching logic on device or browser info in a real application.

Where userAgent() Lives

userAgent() is exported from next/server, alongside NextRequest and NextResponse. That's a signal about where it's meant to be used: request-time code that runs before a page renders, which in the App Router means Proxy (proxy.ts) and Route Handlers — not Server Components, and not the browser.

// proxy.ts
import { NextRequest, NextResponse, userAgent } from "next/server";

export function proxy(request: NextRequest) {
  const url = request.nextUrl;
  const { device } = userAgent(request);

  // device.type can be: 'mobile', 'tablet', 'console', 'smarttv',
  // 'wearable', 'embedded', or undefined (for desktop browsers)
  const viewport = device.type || "desktop";

  url.searchParams.set("viewport", viewport);
  return NextResponse.rewrite(url);
}

That's the canonical example straight from the docs, and it's worth pausing on why it's structured this way. userAgent() takes a Request-like object (specifically NextRequest, which itself extends the standard Web Request) and reads the User-Agent header off of it internally — you never pass the header string yourself. That's a deliberate API choice: it means the same call works whether you're in Proxy or a Route Handler, since both receive a request object of the right shape.

The Full Return Shape

Calling userAgent(request) returns one object with six top-level properties. None of them are optional in the sense of "might not exist" — they always exist as objects — but nearly every individual field inside them can be undefined when the parser can't confidently determine a value.

isBot

A plain boolean. true if the request appears to come from a known bot (search engine crawlers, social media link-preview fetchers, monitoring services, and so on), false otherwise.

const { isBot } = userAgent(request);

if (isBot) {
  // serve a simplified, fully-static response — skip
  // personalization, A/B tests, and anything that assumes
  // a real user is present
}

browser

{
  name: string | undefined; // e.g. "Chrome", "Safari", "Firefox"
  version: string | undefined; // e.g. "124.0.0.0"
}

device

{
  model: string | undefined;
  type: "console" |
    "mobile" |
    "tablet" |
    "smarttv" |
    "wearable" |
    "embedded" |
    undefined;
  vendor: string | undefined;
}

The type field is the one you'll reach for most often in practice — it's the cleanest signal for "is this a phone, a tablet, a TV, or (implicitly, when undefined) a regular desktop browser." Note that undefined here doesn't mean "unknown," it specifically means "this looks like a normal desktop or laptop," which is a slightly unusual convention worth remembering.

engine

{
  name:
    | "Amaya"
    | "Blink"
    | "EdgeHTML"
    | "Flow"
    | "Gecko"
    | "Goanna"
    | "iCab"
    | "KHTML"
    | "Links"
    | "Lynx"
    | "NetFront"
    | "NetSurf"
    | "Presto"
    | "Tasman"
    | "Trident"
    | "w3m"
    | "WebKit"
    | undefined;
  version: string | undefined;
}

This is the rendering engine underneath the browser — Blink for Chrome/Edge/most Chromium browsers, Gecko for Firefox, WebKit for Safari. Most application code never needs this; it's more useful for analytics or for detecting rendering-engine-specific CSS/JS quirks than for typical routing decisions.

os

{
  name: string | undefined; // e.g. "iOS", "Android", "Windows", "Mac OS"
  version: string | undefined;
}

cpu

{
  architecture:
    | "68k"
    | "amd64"
    | "arm"
    | "arm64"
    | "armhf"
    | "avr"
    | "ia32"
    | "ia64"
    | "irix"
    | "irix64"
    | "mips"
    | "mips64"
    | "pa-risc"
    | "ppc"
    | "sparc"
    | "sparc64"
    | undefined;
}

This is the least commonly used field of the six by a wide margin. It exists mostly for niche cases — telemetry that cares about ARM vs. x86 traffic, or download pages that want to pre-select the right binary for a user's architecture.

Practical Pattern: Serving Different Layouts by Device

The documented example (rewriting based on device.type) is really a specific case of a more general and genuinely useful pattern: routing the same URL to different rendered output depending on device class, without the client ever knowing a rewrite happened.

// proxy.ts
import { NextRequest, NextResponse, userAgent } from "next/server";

export function proxy(request: NextRequest) {
  const { device } = userAgent(request);
  const url = request.nextUrl;

  if (device.type === "mobile" && url.pathname === "/dashboard") {
    url.pathname = "/dashboard/mobile";
    return NextResponse.rewrite(url);
  }

  return NextResponse.next();
}

export const config = {
  matcher: "/dashboard",
};

The user still sees /dashboard in their address bar; the App Router silently serves /dashboard/mobile underneath. This is a legitimate alternative to a single responsive layout when the mobile and desktop experiences are different enough — different navigation patterns, different information density — that maintaining one component tree with a thicket of conditional rendering would be worse than two purpose-built routes.

Practical Pattern: Bot-Aware Responses

isBot is a genuinely useful signal for two common scenarios that have nothing to do with each other but both come up constantly:

SEO and link previews. Search engine crawlers and social media bots (Slack, Discord, X, LinkedIn link unfurlers) often can't or won't execute JavaScript, and some have tight timeouts. If a page depends on expensive client-side personalization, you can detect isBot and serve a static, fully-rendered variant instead:

export function proxy(request: NextRequest) {
  const { isBot } = userAgent(request);
  const url = request.nextUrl;

  if (isBot) {
    url.searchParams.set("mode", "static");
    return NextResponse.rewrite(url);
  }

  return NextResponse.next();
}

Excluding bot traffic from analytics or rate limits. If you're logging page views or applying per-visitor rate limiting in Proxy, checking isBot first lets you skip counting crawler traffic as real user activity, or apply a separate (usually stricter) limit to it.

Common Mistakes

Confusing this with client-side user agent sniffing. userAgent() only works with a Request object on the server — it has nothing to do with navigator.userAgent in the browser, and you can't call it from a Client Component. If you need device information inside client-rendered UI (for a responsive component that needs to know device type before hydration, for example), you'll need a different approach: CSS media queries where possible, or reading a value the server already computed and passed down as a prop or cookie.

Treating isBot as a security boundary. The bot list userAgent() checks against covers well-behaved, self-identifying crawlers. It does nothing to stop a scraper that deliberately spoofs a normal browser's user agent string, which is trivial to do. Use isBot for SEO and UX decisions, not for anything that needs to be actually adversarial-resistant — that's a job for rate limiting, CAPTCHAs, or a dedicated bot-management service.

Assuming every field will be populated. Every nested field in the return object is typed as possibly undefined, and that's not defensive typing for the sake of it — real-world user agent strings genuinely fail to parse cleanly often enough that you'll hit undefined in production. Always have a sensible fallback (as the docs' own example does with device.type || 'desktop'), rather than assuming browser.name or os.name will always resolve to a string.

Reaching for this when a simpler signal would do. If all you actually need is "is this a touch device," a client-side CSS @media (pointer: coarse) query is simpler, more accurate, and doesn't require a server round-trip or Proxy execution at all. userAgent() earns its keep specifically when the decision has to be made before any HTML is sent to the client — routing to a different page, varying a cache key, or filtering logs — not for garden-variety responsive design.

Key Takeaways

QuestionAnswer
Where can I call userAgent()?Proxy (proxy.ts) and Route Handlers — anywhere you have a NextRequest-like object
Can I call it from a Server Component or the browser?No — it needs a request object, which Server Components don't receive directly and the browser doesn't have
What does it return?{ isBot, browser, device, engine, os, cpu } — one object, six always-present sub-objects with mostly-optional fields
Best use caseDevice-based rewrites, bot-aware responses, and analytics/rate-limit segmentation at the edge
Is isBot a security feature?No — it's trivially spoofable; use it for SEO/UX, not access control
What if a field is undefined?Expected and common — always provide a fallback rather than assuming a value

userAgent() is a small helper, but it removes a genuinely annoying parsing problem from the one place in a Next.js app — Proxy — where you're most likely to need device or bot information before any rendering has happened. Reach for it when the decision has to be made at the request level; reach for CSS or client-side checks for everything else.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper