Skip to main content
The Accepts helper enables content negotiation by matching request Accept headers (like Accept, Accept-Language, Accept-Encoding) against your supported options. It returns the best match based on client preferences and quality values.

Import

import { accepts } from 'hono/accepts'

Basic Usage

Match the Accept header to determine response format:
import { accepts } from 'hono/accepts'

app.get('/data', (c) => {
  const format = accepts(c, {
    header: 'Accept',
    supports: ['application/json', 'text/html'],
    default: 'application/json',
  })
  
  if (format === 'text/html') {
    return c.html('<h1>Data</h1>')
  }
  
  return c.json({ data: 'value' })
})

Parameters

c
Context
required
The Hono context object
options
acceptsOptions
required
Configuration object for content negotiation

Options Object

options.header
AcceptHeader
required
The header to match against. Common values:
  • 'Accept' - Content type
  • 'Accept-Language' - Language preference
  • 'Accept-Encoding' - Compression method
options.supports
string[]
required
Array of values your application supports (e.g., ['en', 'ja', 'zh'])
options.default
string
required
Default value to return if no match is found or header is missing
options.match
function
Custom matching function to override default behavior. Receives parsed accepts array and config.
Returns: string - The best matching supported value or the default

Content Type Negotiation

Match the Accept header for API responses:
import { accepts } from 'hono/accepts'

app.get('/users', (c) => {
  const type = accepts(c, {
    header: 'Accept',
    supports: ['application/json', 'application/xml', 'text/html'],
    default: 'application/json',
  })
  
  const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
  
  switch (type) {
    case 'text/html':
      return c.html(`<ul>${users.map(u => `<li>${u.name}</li>`).join('')}</ul>`)
    case 'application/xml':
      return c.text(`<users>${users.map(u => `<user><name>${u.name}</name></user>`).join('')}</users>`, 200, {
        'Content-Type': 'application/xml'
      })
    default:
      return c.json(users)
  }
})

Language Negotiation

Automatic language selection based on user preferences:
import { accepts } from 'hono/accepts'

app.get('/*', async (c, next) => {
  const lang = accepts(c, {
    header: 'Accept-Language',
    supports: ['en', 'ja', 'zh', 'es'],
    default: 'en',
  })
  
  // Store language in context for later use
  c.set('lang', lang)
  
  // Redirect to language-specific path if needed
  const hasLangPrefix = /^\/(en|ja|zh|es)/.test(c.req.path)
  if (!hasLangPrefix) {
    return c.redirect(`/${lang}${c.req.path}`)
  }
  
  await next()
})

app.get('/:lang/welcome', (c) => {
  const messages = {
    en: 'Welcome!',
    ja: 'ようこそ!',
    zh: '欢迎!',
    es: '¡Bienvenido!',
  }
  const lang = c.req.param('lang') as keyof typeof messages
  return c.text(messages[lang])
})

Compression Negotiation

Select compression method based on client support:
import { accepts } from 'hono/accepts'

app.get('/compressed', async (c) => {
  const encoding = accepts(c, {
    header: 'Accept-Encoding',
    supports: ['gzip', 'deflate', 'br'],
    default: 'identity',
  })
  
  const data = 'Large response data...'
  const readable = new ReadableStream({
    start(controller) {
      controller.enqueue(new TextEncoder().encode(data))
      controller.close()
    },
  })
  
  if (encoding === 'gzip') {
    c.header('Content-Encoding', 'gzip')
    return c.body(readable.pipeThrough(new CompressionStream('gzip')))
  }
  
  if (encoding === 'deflate') {
    c.header('Content-Encoding', 'deflate')
    return c.body(readable.pipeThrough(new CompressionStream('deflate')))
  }
  
  if (encoding === 'br') {
    c.header('Content-Encoding', 'br')
    // Brotli compression if supported by runtime
    return c.body(readable.pipeThrough(new CompressionStream('br')))
  }
  
  return c.body(data)
})

Custom Matching Logic

Provide a custom match function for specialized behavior:
import { accepts } from 'hono/accepts'
import type { Accept, acceptsConfig } from 'hono/accepts'

app.get('/priority', (c) => {
  // Custom matcher that prefers lower quality values (unusual but demonstrates flexibility)
  const customMatch = (accepts: Accept[], config: acceptsConfig): string => {
    const { supports, default: defaultSupport } = config
    const accept = accepts
      .sort((a, b) => a.q - b.q) // Sort ascending instead of descending
      .find((accept) => supports.includes(accept.type))
    return accept ? accept.type : defaultSupport
  }
  
  const format = accepts(c, {
    header: 'Accept',
    supports: ['application/json', 'text/html'],
    default: 'application/json',
    match: customMatch,
  })
  
  return c.json({ selectedFormat: format })
})

Quality Values

The helper automatically respects quality values (q-values) in Accept headers:
// Request with header:
// Accept: text/html,application/xml;q=0.9,application/json;q=0.8

const type = accepts(c, {
  header: 'Accept',
  supports: ['application/json', 'application/xml', 'text/html'],
  default: 'text/html',
})
// Returns: 'text/html' (highest quality, q=1.0 by default)
// Request with header:
// Accept-Language: en;q=0.8,ja

const lang = accepts(c, {
  header: 'Accept-Language',
  supports: ['en', 'ja', 'zh'],
  default: 'en',
})
// Returns: 'ja' (q=1.0 is higher than en's q=0.8)

Type Definitions

interface Accept {
  type: string
  params: Record<string, string>
  q: number // Quality value (0-1)
}

interface acceptsConfig {
  header: AcceptHeader
  supports: string[]
  default: string
}

interface acceptsOptions extends acceptsConfig {
  match?: (accepts: Accept[], config: acceptsConfig) => string
}

type AcceptHeader = 
  | 'Accept'
  | 'Accept-Language' 
  | 'Accept-Encoding'
  | string

function accepts(c: Context, options: acceptsOptions): string

Default Matching Behavior

The default matcher:
  1. Parses the Accept header into an array of Accept objects
  2. Sorts by quality value (q) in descending order
  3. Finds the first value that matches your supports array
  4. Returns the matched value or the default if no match
const defaultMatch = (accepts: Accept[], config: acceptsConfig): string => {
  const { supports, default: defaultSupport } = config
  const accept = accepts
    .sort((a, b) => b.q - a.q)
    .find((accept) => supports.includes(accept.type))
  return accept ? accept.type : defaultSupport
}

Best Practices

Always Provide Default

Always specify a sensible default value for cases where the header is missing or no match is found

Order Matters

List supported values in order of preference when multiple have the same quality value

Respect Standards

Use standard MIME types for Accept header (application/json, not json)

Test Edge Cases

Test with missing headers and wildcard values like */*
When the Accept header is missing or empty, the helper returns the default value immediately without attempting to match.

Build docs developers (and LLMs) love