userAgent()

Parses the request's User-Agent header into device, browser, engine, os and cpu details.

Signature
function userAgent(request: { headers: Headers }): UserAgent

Parameters

ParameterTypeDescription
$request{ headers: Headers }Any object carrying a Web Headers instance — typically the NextRequest passed to proxy.js or a Route Handler.

Return value

UserAgentAn object with isBot, browser { name, version }, device { model, type, vendor }, engine { name, version }, os { name, version } and cpu { architecture }. Every string field may be undefined.

userAgent() reads the user-agent header off a request and returns a parsed structure instead of a string you would otherwise have to regex. It is a plain synchronous helper — pass it anything with a headers property, which in practice is the NextRequest in proxy.js or a Route Handler.

When to reach for it

Use it to branch on shape of client rather than identity of user: rewriting to a mobile-optimised route, skipping an expensive personalisation step for crawlers via isBot, or recording browser and OS breakdown in analytics.

What trips people up

  • Every field can be undefined. Desktop browsers report no device.type at all — that is why the canonical example writes device.type || 'desktop'.
  • User-Agent strings are client-controlled and freely spoofed. Never gate authorization or security decisions on isBot.
  • Branching the HTML on device type in a cached route will serve whichever variant was stored first. Rewrite to a distinct URL, as below, so each variant gets its own cache entry.
  • Modern browsers have flattened their UA strings, so browser.version is coarse; prefer feature detection on the client for anything behavioural.

Example

Example · typescript
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)
}

When to use it

  • Rewrite mobile visitors to a lighter route from proxy.js
  • Skip personalisation and analytics for crawlers using isBot
  • Record browser and OS breakdown alongside a Route Handler's response
  • Serve a different Open Graph image size to a known social scraper

Frequently asked questions

How do I detect a mobile device in Next.js?

Call userAgent(request) in proxy.js and read device.type, falling back to 'desktop' when it is undefined — desktop browsers report no device type. Then rewrite to a mobile route rather than branching inside one cached page.

Is isBot reliable enough to block scrapers?

No. It matches known crawler signatures in a client-supplied header, and any client can send whatever User-Agent it likes. Use it to skip personalisation or experiments, never as a security or access control check.

Where can I call userAgent()?

Anywhere you hold an object with a headers property — proxy.js and Route Handlers are the usual places, since both receive a NextRequest. In a Server Component you can build the argument yourself from await headers().

More examples

Skipping work for crawlers

isBot is a single boolean derived from known crawler signatures. Passing it downstream as a header lets pages cheapen their render for crawlers without re-parsing the UA. Treat it as a hint only — it is trivially spoofed in either direction, so it must never guard access to anything.

Example · typescript
import { NextRequest, NextResponse, userAgent } from 'next/server'

export function proxy(request: NextRequest) {
  const { isBot } = userAgent(request)

  const response = NextResponse.next()
  response.headers.set('x-is-bot', isBot ? '1' : '0')

  if (isBot) {
    // Don't assign crawlers to experiments
    response.cookies.delete('experiment-bucket')
  }

  return response
}

Logging the parsed shape from a Route Handler

This is the full shape of the return value in one place. Every string is optional, so each field is normalised with ?? before serialising — otherwise undefined keys silently vanish from the JSON and downstream consumers see a different object shape per client.

Example · typescript
import { NextRequest, userAgent } from 'next/server'

export function GET(request: NextRequest) {
  const { browser, os, engine, cpu, device, isBot } = userAgent(request)

  return Response.json({
    isBot,
    browser: browser.name ?? null,
    browserVersion: browser.version ?? null,
    os: os.name ?? null,
    engine: engine.name ?? null,
    architecture: cpu.architecture ?? null,
    deviceType: device.type ?? 'desktop',
  })
}

Rewriting to a separate route so caching stays correct

Mobile clients get /m rendered while their address bar still shows /. Because the two variants are distinct routes, each has its own cache entry — the failure mode this avoids is branching inside a single cached page, where whichever device happened to warm the cache first fixes the HTML for everyone.

Example · typescript
import { NextRequest, NextResponse, userAgent } from 'next/server'

export const config = {
  matcher: '/',
}

export function proxy(request: NextRequest) {
  const { device } = userAgent(request)

  if (device.type === 'mobile') {
    const url = request.nextUrl.clone()
    url.pathname = '/m'
    return NextResponse.rewrite(url)
  }

  return NextResponse.next()
}

Discussion

  • Be the first to comment on this lesson.