userAgent()
Parses the request's User-Agent header into device, browser, engine, os and cpu details.
function userAgent(request: { headers: Headers }): UserAgentParameters
| Parameter | Type | Description |
|---|---|---|
$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 nodevice.typeat all — that is why the canonical example writesdevice.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.versionis coarse; prefer feature detection on the client for anything behavioural.
Example
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.
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.
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.
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