Skip to main content

Overview

The Context object (c) is passed to every handler and middleware function. It provides access to the request, response, environment variables, and utility methods for building responses. From src/context.ts:293-299:
export class Context<
  E extends Env = any,
  P extends string = any,
  I extends Input = {},
> {
  // Context implementation
}

Request Access

Access the request object through c.req:
app.get('/users/:id', (c) => {
  // Get path parameters
  const id = c.req.param('id')
  const params = c.req.param() // All params

  // Get query parameters
  const search = c.req.query('search')
  const queries = c.req.queries() // All queries

  // Get headers
  const auth = c.req.header('Authorization')
  const headers = c.req.header() // All headers

  // Get request method and path
  const method = c.req.method
  const path = c.req.path
  const url = c.req.url

  return c.json({ id, search, auth, method, path })
})
From src/context.ts:364-369:
/**
 * `.req` is the instance of {@link HonoRequest}.
 */
get req(): HonoRequest<P, I['out']> {
  this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult)
  return this.#req
}

Response Methods

The Context provides several methods for returning responses:

Text Response

Return plain text:
app.get('/hello', (c) => {
  return c.text('Hello, World!')
})

app.get('/custom', (c) => {
  return c.text('Created', 201)
})
From src/context.ts:670-694:
text: TextRespond = (
  text: string,
  arg?: ContentfulStatusCode | ResponseOrInit,
  headers?: HeaderRecord
): ReturnType<TextRespond> => {
  return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized
    ? (new Response(text) as ReturnType<TextRespond>)
    : (this.#newResponse(
        text,
        arg,
        setDefaultContentType(TEXT_PLAIN, headers)
      ) as ReturnType<TextRespond>)
}

JSON Response

Return JSON data:
app.get('/api/users', (c) => {
  return c.json({
    users: [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ]
  })
})

app.post('/api/users', async (c) => {
  const user = await c.req.json()
  return c.json({ created: user }, 201)
})
From src/context.ts:696-721:
json: JSONRespond = <
  T extends JSONValue | {} | InvalidJSONValue,
  U extends ContentfulStatusCode = ContentfulStatusCode,
>(
  object: T,
  arg?: U | ResponseOrInit<U>,
  headers?: HeaderRecord
): JSONRespondReturn<T, U> => {
  return this.#newResponse(
    JSON.stringify(object),
    arg,
    setDefaultContentType('application/json', headers)
  ) as any
}

HTML Response

Return HTML content:
app.get('/', (c) => {
  return c.html('<h1>Hello Hono!</h1>')
})

app.get('/page', (c) => {
  return c.html(`
    <!DOCTYPE html>
    <html>
      <head><title>My Page</title></head>
      <body><h1>Welcome</h1></body>
    </html>
  `)
})
From src/context.ts:723-733:
html: HTMLRespond = (
  html: string | Promise<string>,
  arg?: ContentfulStatusCode | ResponseOrInit<ContentfulStatusCode>,
  headers?: HeaderRecord
): Response | Promise<Response> => {
  const res = (html: string) =>
    this.#newResponse(html, arg, setDefaultContentType('text/html; charset=UTF-8', headers))
  return typeof html === 'object'
    ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res)
    : res(html)
}

Redirect

Redirect to another URL:
app.get('/old-path', (c) => {
  return c.redirect('/new-path')
})

app.get('/external', (c) => {
  return c.redirect('https://example.com', 301)
})
From src/context.ts:735-762:
redirect = <T extends RedirectStatusCode = 302>(
  location: string | URL,
  status?: T
): Response & TypedResponse<undefined, T, 'redirect'> => {
  const locationString = String(location)
  this.header(
    'Location',
    !/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString)
  )
  return this.newResponse(null, status ?? 302) as any
}

Body Response

Return raw body data:
app.get('/file', (c) => {
  const data = new Uint8Array([1, 2, 3])
  return c.body(data)
})

app.get('/stream', (c) => {
  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue('chunk 1')
      controller.enqueue('chunk 2')
      controller.close()
    }
  })
  return c.body(stream)
})
From src/context.ts:643-668:
body: BodyRespond = (
  data: Data | null,
  arg?: StatusCode | RequestInit,
  headers?: HeaderRecord
): ReturnType<BodyRespond> => this.#newResponse(data, arg, headers) as ReturnType<BodyRespond>

Headers

Setting Headers

app.get('/api/data', (c) => {
  c.header('X-Custom-Header', 'value')
  c.header('Cache-Control', 'no-cache')
  return c.json({ data: 'example' })
})
From src/context.ts:500-527:
header: SetHeaders = (name, value, options): void => {
  if (this.finalized) {
    this.#res = createResponseInstance((this.#res as Response).body, this.#res)
  }
  const headers = this.#res ? this.#res.headers : (this.#preparedHeaders ??= new Headers())
  if (value === undefined) {
    headers.delete(name)
  } else if (options?.append) {
    headers.append(name, value)
  } else {
    headers.set(name, value)
  }
}

Appending Headers

app.get('/multi-header', (c) => {
  c.header('Set-Cookie', 'session=abc', { append: true })
  c.header('Set-Cookie', 'token=xyz', { append: true })
  return c.text('Headers set')
})

Status Code

Set HTTP status code:
app.post('/api/users', async (c) => {
  c.status(201)
  return c.json({ message: 'User created' })
})

app.delete('/api/users/:id', (c) => {
  c.status(204)
  return c.body(null)
})
From src/context.ts:529-531:
status = (status: StatusCode): void => {
  this.#status = status
}

Context Variables

Store and retrieve values during request processing:

Setting Variables

app.use('*', async (c, next) => {
  c.set('requestId', crypto.randomUUID())
  c.set('startTime', Date.now())
  await next()
})
From src/context.ts:533-556:
set: Set<
  IsAny<E> extends true
    ? {
        Variables: ContextVariableMap & Record<string, any>
      }
    : E
> = (key: string, value: unknown) => {
  this.#var ??= new Map()
  this.#var.set(key, value)
}

Getting Variables

app.get('/profile', (c) => {
  const requestId = c.get('requestId')
  const user = c.get('user')
  return c.json({ requestId, user })
})
From src/context.ts:558-580:
get: Get<
  IsAny<E> extends true
    ? {
        Variables: ContextVariableMap & Record<string, any>
      }
    : E
> = (key: string) => {
  return this.#var ? this.#var.get(key) : undefined
}

Accessing Variables with .var

Read-only access to all variables:
app.get('/info', (c) => {
  const vars = c.var
  // Access all context variables as object
  return c.json(vars)
})
From src/context.ts:582-602:
get var(): Readonly<
  ContextVariableMap & (IsAny<E['Variables']> extends true ? Record<string, any> : E['Variables'])
> {
  if (!this.#var) {
    return {} as any
  }
  return Object.fromEntries(this.#var)
}

Type-Safe Variables

Define variable types for type safety:
type Env = {
  Variables: {
    user: { id: string; email: string }
    requestId: string
  }
}

const app = new Hono<Env>()

app.use('*', async (c, next) => {
  c.set('requestId', crypto.randomUUID())
  c.set('user', { id: '123', email: '[email protected]' })
  await next()
})

app.get('/profile', (c) => {
  const user = c.get('user') // Type: { id: string; email: string }
  const requestId = c.get('requestId') // Type: string
  return c.json({ user, requestId })
})

Environment Variables

Access environment bindings (Cloudflare Workers, etc.):
type Env = {
  Bindings: {
    DB: D1Database
    BUCKET: R2Bucket
    API_KEY: string
  }
}

const app = new Hono<Env>()

app.get('/data', async (c) => {
  const db = c.env.DB
  const bucket = c.env.BUCKET
  const apiKey = c.env.API_KEY

  const result = await db.prepare('SELECT * FROM users').all()
  return c.json(result)
})
From src/context.ts:302-315:
/**
 * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers.
 *
 * @example
 * ```ts
 * // Environment object for Cloudflare Workers
 * app.get('*', async c => {
 *   const counter = c.env.COUNTER
 * })
 * ```
 */
env: E['Bindings'] = {}

Execution Context

Access the execution context (for Cloudflare Workers):
app.get('/task', async (c) => {
  // Schedule background task
  c.executionCtx.waitUntil(
    fetch('https://api.example.com/log', {
      method: 'POST',
      body: JSON.stringify({ request: c.req.path })
    })
  )

  return c.text('Task scheduled')
})
From src/context.ts:385-397:
get executionCtx(): ExecutionContext {
  if (this.#executionCtx) {
    return this.#executionCtx as ExecutionContext
  } else {
    throw Error('This context has no ExecutionContext')
  }
}
From src/context.ts:29-52:
export interface ExecutionContext {
  /**
   * Extends the lifetime of the event callback until the promise is settled.
   */
  waitUntil(promise: Promise<unknown>): void
  /**
   * Allows the event to be passed through to subsequent event listeners.
   */
  passThroughOnException(): void
}

Rendering

Custom Renderer

Set a custom renderer for templating:
import { html } from 'hono/html'

app.use('*', async (c, next) => {
  c.setRenderer((content, props) => {
    return c.html(html`
      <!DOCTYPE html>
      <html>
        <head><title>${props?.title || 'My App'}</title></head>
        <body>
          <header><h1>${props?.title}</h1></header>
          <main>${content}</main>
        </body>
      </html>
    `)
  })
  await next()
})

app.get('/', (c) => {
  return c.render('Welcome to Hono!', { title: 'Home' })
})
From src/context.ts:437-497:
render: Renderer = (...args) => {
  this.#renderer ??= (content: string | Promise<string>) => this.html(content)
  return this.#renderer(...args)
}

setRenderer = (renderer: Renderer): void => {
  this.#renderer = renderer
}

Error Property

Access errors thrown in middleware:
app.use('*', async (c, next) => {
  await next()
  
  if (c.error) {
    console.error('Error occurred:', c.error)
  }
})
From src/context.ts:317-333:
/**
 * `.error` can get the error object from the middleware if the Handler throws an error.
 *
 * @example
 * ```ts
 * app.use('*', async (c, next) => {
 *   await next()
 *   if (c.error) {
 *     // do something...
 *   }
 * })
 * ```
 */
error: Error | undefined

Response Access

Access or set the response:
app.use('*', async (c, next) => {
  await next()
  
  // Access response
  console.log(c.res.status)
  console.log(c.res.headers)
})
From src/context.ts:399-434:
get res(): Response {
  return (this.#res ||= createResponseInstance(null, {
    headers: (this.#preparedHeaders ??= new Headers()),
  }))
}

set res(_res: Response | undefined) {
  if (this.#res && _res) {
    _res = createResponseInstance(_res.body, _res)
    for (const [k, v] of this.#res.headers.entries()) {
      if (k === 'content-type') {
        continue
      }
      if (k === 'set-cookie') {
        const cookies = this.#res.headers.getSetCookie()
        _res.headers.delete('set-cookie')
        for (const cookie of cookies) {
          _res.headers.append('set-cookie', cookie)
        }
      } else {
        _res.headers.set(k, v)
      }
    }
  }
  this.#res = _res
  this.finalized = true
}

Not Found Handler

Trigger the not found handler:
app.get('/users/:id', async (c) => {
  const user = await findUser(c.req.param('id'))
  
  if (!user) {
    return c.notFound()
  }
  
  return c.json(user)
})
From src/context.ts:764-779:
notFound = (): ReturnType<NotFoundHandler> => {
  this.#notFoundHandler ??= () => createResponseInstance()
  return this.#notFoundHandler(this)
}

Best Practices

  • Use TypeScript to define environment and variable types
  • Set context variables in middleware for cross-handler data sharing
  • Use appropriate response methods (json, text, html) for content types
  • Access c.req for all request data instead of the raw request object
  • Leverage c.executionCtx for background tasks in Cloudflare Workers
Don’t modify the raw request object. Use Context methods and properties for all request/response operations.

Build docs developers (and LLMs) love