Request
Read body, query string, route parameters, headers, and cookies from the incoming request.
Introduction
Every handler receives an HttpContext whose request property is a rich TekirRequest wrapper around Bun's native Request. It provides typed helpers for reading body data, query strings, headers, and cookies without reaching for the underlying Fetch API.
import type { HttpContext } from '@tekir/core'
export function store(ctx: HttpContext) {
// ctx.request → TekirRequest (the rich wrapper documented here)
// ctx.body → shortcut for the parsed request body
// ctx.query → shortcut for the parsed query string
// ctx.params → shortcut for URL route parameters
// ctx.headers → shortcut for headers as a plain object
}The Request Object
The full shape of TekirRequest:
interface TekirRequest {
// Properties
raw: Request // the underlying Bun Request
url: string // full URL string
method: string // HTTP method (GET, POST, …)
path: string // pathname, e.g. '/api/users/1'
hostname: string // domain, e.g. 'example.com'
protocol: string // 'http' or 'https'
ip: string // client IP address
ips: string[] // proxy chain (X-Forwarded-For)
completeUrl: string // alias for url
// Methods documented below
all()
input(key, defaultValue?)
only(keys)
except(keys)
qs()
param(key, defaultValue?)
params()
hasBody()
header(name, defaultValue?)
headers()
is(types)
accepts(types)
language(languages)
languages()
cookie(name)
signedCookie(name, secret)
cookies()
id()
matchesRoute(name)
}Body Methods
tekir parses the request body automatically before your handler runs (for JSON, URL-encoded, and multipart/form-data content types). The parsed result is available as ctx.body and through the methods below.
Query string and body parsing are hardened against prototype pollution by default. Dangerous keys such as __proto__, constructor, and prototype are rejected, and the objects returned by all(), only(), except(), and qs() are built without a prototype chain. A request like ?__proto__[admin]=true cannot pollute Object.prototype or leak into other objects, so you can merge request input safely without extra guarding.
all()
Returns a shallow merge of the query string and parsed body. Body fields win on collision. Use this when you don't care whether a value came from the URL or the body.
export async function search({ request }: HttpContext) {
// Merges query string + parsed body into one flat object.
// Body fields take priority over query string fields on collision.
const data = request.all()
// e.g. POST /search?sort=asc with body { q: 'tekir' }
// → { sort: 'asc', q: 'tekir' }
return data
}input(key, defaultValue?)
Looks up a single value by key. The lookup order is: body → query string → route params. Returns the default value (or undefined) if the key is absent everywhere.
export async function login({ request, response }: HttpContext) {
// Looks in body first, then query string, then route params.
const email = request.input('email')
const password = request.input('password')
// With a default value (returned when the key is absent)
const remember = request.input('remember', false)
if (!email || !password) {
return response.badRequest({ message: 'email and password required' })
}
// ...
}only(keys)
Returns a new object containing only the specified keys from all(). Ideal for safe mass-assignment, any extra keys the client might have sent are silently discarded.
export async function update({ request, response }: HttpContext) {
// Accept only these keys from the merged body + query: ignore everything else.
const data = request.only(['name', 'email', 'bio'])
// Safe mass-assignment: even if the client sends 'role' or 'password', they are excluded.
return response.ok(data)
}except(keys)
The inverse of only(): returns all keys except the ones listed.
export async function store({ request, response }: HttpContext) {
// Grab all fields EXCEPT the ones listed.
const data = request.except(['_token', '_method'])
// Useful when you want to strip CSRF tokens or method-spoofing fields
// without knowing every key in advance.
return response.created(data)
}hasBody()
Returns true if a parsed body is present and not null. Useful for guarding PUT or PATCH handlers that require a body.
export async function update({ request, response }: HttpContext) {
if (!request.hasBody()) {
return response.badRequest({ message: 'No body provided' })
}
// Safe to read request.all() / request.input() ...
}ctx.bodyError
When the declared Content-Type does not match the actual payload (an empty body sent with Content-Type: application/json, malformed urlencoded, broken multipart, etc.) the parse error is captured on ctx.bodyError instead of crashing the route. Check it before reading ctx.body if you want to return a real 400:
import type { HttpContext } from '@tekir/core'
export async function store({ body, bodyError, response }: HttpContext) {
if (bodyError) {
return response.badRequest({
message: 'Invalid request body',
detail: bodyError.message,
})
}
// Safe to read body now: parsing succeeded.
return response.created({ ok: true, body })
}Query String
qs()
Returns the raw query string as a Record<string, string | string[]>: keys that appear multiple times become an array. Unlike all(), this never includes the body.
export async function search({ request, response }: HttpContext) {
// qs() returns only the query string, never the body.
const q = request.qs()
// GET /search?q=tekir&tags=fast&tags=small
// → { q: 'tekir', tags: ['fast', 'small'] }
// Repeated keys become arrays automatically.
const term = q.q as string
const tags = Array.isArray(q.tags) ? q.tags : q.tags ? [q.tags] : []
return response.ok({ term, tags })
}Route Parameters
params() and param()
params() returns all route parameters as a plain object. param(key, default?) reads a single parameter with an optional fallback. Values are always strings unless a .where() matcher with a cast function was applied.
export async function showPost({ request, params, response }: HttpContext) {
// params is a shortcut for ctx.params: same object.
// request.params() returns the same thing.
const { userId, postId } = request.params()
// → { userId: '42', postId: '7' } (always strings unless cast by .where())
// Read a single param with a fallback
const id = request.param('userId', '0')
return response.ok({ userId, postId })
}Headers
header(name, defaultValue?)
Read a single request header by name. The lookup is case-insensitive. Returns the default value (or undefined) when the header is absent.
export async function index({ request, response }: HttpContext) {
// Read a single header by name (case-insensitive)
const contentType = request.header('content-type')
// → 'application/json' or undefined
// With a default value
const lang = request.header('accept-language', 'en')
return response.ok({ contentType, lang })
}headers()
Returns all request headers as a plain Record<string, string> object.
export async function headers({ request, response }: HttpContext) {
// All request headers as a plain { name: value } object
const all = request.headers()
// → { 'content-type': 'application/json', 'authorization': 'Bearer ...', ... }
return response.ok(all)
}is(types)
Returns true if the Content-Type header includes any of the given strings. Use it to validate the body format before attempting to parse it.
export async function upload({ request, response }: HttpContext) {
// Returns true if the Content-Type header includes any of the given strings
if (!request.is(['multipart/form-data'])) {
return response.unsupportedMediaType({ message: 'Must be multipart/form-data' })
}
if (request.is(['application/json'])) { /* ... */ }
if (request.is(['text/', 'html'])) { /* ... */ }
}accepts(types)
Inspects the Accept header and returns the first type from your list that the client accepts, or false if there is no match. Use it for content negotiation.
export async function data({ request, response }: HttpContext) {
// Returns the first matched Accept type, or false if none match.
const format = request.accepts(['application/json', 'text/csv'])
if (format === 'application/json') return response.ok({ data: [] })
if (format === 'text/csv') return response.text('col1,col2\n')
return response.notAcceptable()
}language(languages)
Inspects the Accept-Language header and returns the first language from your list that the client accepts, or null if none match. Use languages() (no argument) to get all client-accepted languages sorted by preference.
export async function welcome({ request, response }: HttpContext) {
// Returns the first language from your list that the client accepts
const lang = request.language(['tr', 'en', 'de'])
// Accept-Language: de-DE,de;q=0.9,en;q=0.8
// → 'de'
// null if none match
if (!lang) return response.ok({ greeting: 'Hello' })
return response.ok({ greeting: lang === 'tr' ? 'Merhaba' : lang === 'de' ? 'Hallo' : 'Hello' })
// Get all accepted languages sorted by preference
const all = request.languages()
// → ['de', 'en']
}Cookies
cookie(name)
Read a plain cookie by name. Returns null if the cookie does not exist.
export async function me({ request, response }: HttpContext) {
// Read a plain (unsigned) cookie by name: returns null if absent
const theme = request.cookie('theme')
// → 'dark' or null
return response.ok({ theme })
}signedCookie(name, secret)
Read a signed cookie. tekir verifies the HMAC signature using the provided secret before returning the value. Returns null if the cookie is absent, malformed, or the signature does not match, preventing tampering.
import env from '#env'
export async function cart({ request, response }: HttpContext) {
// Read a signed cookie: verifies the HMAC signature automatically.
// Returns null if the cookie is absent OR tampered with.
const cartId = request.signedCookie('cart_id', env.APP_KEY)
if (!cartId) return response.ok({ items: [] })
return response.ok({ cartId })
}id()
Returns a unique identifier for this request. tekir uses the value of the X-Request-Id header if present; otherwise it generates a UUID. The same value is returned every time id() is called within a single request lifecycle, so you can use it for correlating log lines.
export function index({ request }: HttpContext) {
// Returns the value of the X-Request-Id header if present,
// or a newly generated UUID for this request: stable for the request lifetime.
const requestId = request.id()
// → 'a1b2c3d4-...'
}Raw Request
If you need access to something that TekirRequest does not expose, the underlying Bun Request is always available as request.raw.
export async function webhook({ request, response }: HttpContext) {
// Access the underlying Bun Request for anything not covered by TekirRequest
const rawBody = await request.raw.text()
const signature = request.header('x-signature')
if (!verifySignature(rawBody, signature)) {
return response.unauthorized()
}
const event = JSON.parse(rawBody)
return response.noContent()
}