Reading the request
Params, search params, body, cookies, negotiation.
Inside a handler you read the request through composables. These are small functions like useRequest and useSearchParams that you call instead of passing a request object through your code. They work anywhere inside a request, however deep the call, and middleware can use them too.
import { defineHandler, useSearchParams } from 'ohnejs';
export default defineHandler(() => {
const { q } = useSearchParams();
return { query: q };
});Search params
useSearchParams() parses the URL query into a structured object. Values carry their own type:
// GET /search?q=ohne&page=2&draft=true&tags=[new,sale]
useSearchParams() // -> { q: 'ohne', page: 2, draft: true, tags: ['new', 'sale'] }The grammar:
1is a number,trueandfalsebooleans,nullisnull.[a,b]is a list,{k:v}an object, and both nest freely.- A leading backtick forces a string:
`1is the string'1'. - A key with no value is
true:?draftis{ draft: true }. - A repeated key becomes an array:
?t=1&t=2is{ t: [1, 2] }.
The grammar reads the query as written, so ?tags=%5Bnew%5D is the string '[new]', not a list. URLSearchParams and encodeURIComponent encode brackets, braces, :, , and the backtick, so build such a URL with stringifySearchParams from ohnejs/utils.
Typed values are not always what you want: ?id=42 arrives as the number 42, not the string '42'. When a value must arrive as text, whatever it looks like, send it with the backtick: ?id=`42 is '42'.
The coercion is careful, and untrusted input never throws:
- Only JSON-shaped numbers convert.
?id=007stays text, since JSON numbers have no leading zero. - An integer outside the safe range stays text, so it keeps every digit.
- A malformed value falls back to its decoded string.
- Nesting is limited to 32 levels.
For the raw, uncoerced URLSearchParams, read useEvent().url.searchParams.
Querying over HTTP uses this same grammar. There, where, order, and the other query options are structured values in the URL.
The body
There is one reader for each body shape, and each works only inside a request:
import { defineHandler, readJSONBody } from 'ohnejs';
export default defineHandler(async () => {
const input = await readJSONBody<{ email: string }>();
return { subscribed: input.email };
});readJSONBody<T>()parses a JSON body. TheContent-Typemust beapplication/jsonor carry a+jsonsuffix, and anything else rejects with415. An empty or malformed body rejects with400, and so does one nested deeper thanmaxDepth, 64 levels unless you pass another. The result is typed asTbut not checked, so validate it before you trust it.readTextBody()reads the body as a UTF-8 string,''when there is none. Invalid UTF-8 rejects with400.readFormBody()reads a form intoFormData. It acceptsmultipart/form-dataandapplication/x-www-form-urlencoded. Any otherContent-Typerejects with415, and a malformed body with400. A missing body gives an emptyFormData.readRawBody()returns the bytes as aUint8Array, orundefinedwhen there is no body.
The body is read once and remembered, so every reader reuses the same bytes. Read it as many times as you like, in any shape.
api.maxBodySize limits the body size, '1mb' by default, and a larger body is refused with 413. Raise the limit in config, or per route.
A client that sends Expect: 100-continue, as curl does for a large body, waits before sending it. The server tells it to go ahead when your handler first reads the body. If your handler answers before reading, for example with a 401, the body is never sent. A client stops waiting after a while on its own, about a second for curl. A proxy that buffers request bodies, as nginx does by default, takes the whole body anyway.
Cookies
useCookies() parses the Cookie header into a map of name to value:
// Cookie: id=42; theme=dark
useCookies() // -> { id: '42', theme: 'dark' }setCookie appends a Set-Cookie header, one call per cookie, and deleteCookie clears one by expiring it. Pass the same domain and path the cookie was set with, since a browser only clears a matching cookie:
setCookie('theme', 'dark', { path: '/', maxAge: 31536000 });
deleteCookie('theme', { path: '/' });The options cover every cookie attribute: domain, path, expires, maxAge, httpOnly, secure, sameSite, partitioned, and priority.
Signed cookies
A signed cookie proves that nobody changed the value after it left your server. setSignedCookie signs with the COOKIE_SECRET env var and defaults to httpOnly, secure, and sameSite: 'lax', the safe settings for a session:
setSignedCookie('session', userId);useSignedCookies() returns only the cookies whose signature is valid. Changed, unsigned, and renamed ones are dropped. The signature covers the cookie's name too, so a valid value cannot be reused under another name.
useSignedCookies() // -> { session: 'u42' }- Signing is not encryption. Anyone can still read the value, so never sign a secret.
- With no
COOKIE_SECRETset, signing throws an error instead of signing with an empty key.
Route params
useRouteParams() returns the same params the handler receives, from anywhere in the request: useRouteParams().id reads an [id] segment. Route params covers the naming. Annotating the handler's context narrows params to specific keys.
Content negotiation
useAccepts takes the media types you can produce and picks the best one for the Accept header. useAcceptsLanguages does the same for Accept-Language:
// Accept-Language: de-AT, de;q=0.9, en;q=0.5
useAcceptsLanguages(['en', 'de']) // -> 'de'- Both return the chosen offer, or
undefinedwhen the client accepts none of them. - Both append the header they read to the response
Vary. - A request without the header accepts anything, so the first offer wins. Put your preferred format first.
Authorization
useAuthorization() parses the Authorization header into its scheme and credentials, or returns null when the header is missing or has no credentials. The scheme is lowercased, and the token is kept exactly as sent. A basic token is also decoded into username and password:
// Authorization: Bearer abc.def
useAuthorization() // -> { scheme: 'bearer', token: 'abc.def' }
// Authorization: Basic dXNlcjpwYXNz
useAuthorization()
// -> { scheme: 'basic', token: 'dXNlcjpwYXNz', username: 'user', password: 'pass' }Sessions also accept a Bearer token in this header.
Language
Every request has a language, and useT() translates into it. ohne negotiates it from Accept-Language, or a middleware forces it through context.locale.
The request
useRequest() returns the incoming request as the Web standard Request, shorthand for useEvent().request. You get the method, the headers, and everything else the standard type offers:
useRequest().method // -> 'POST'
useRequest().headers.get('user-agent') // -> the header valueWhen the client disconnects before you respond, for example by closing the tab, the request's signal aborts. Pass it to a slow call that accepts a signal, such as fetch. The call then stops early, since nobody is left to receive its result:
const report = await fetch('https://reports.example.com/daily', { signal: useRequest().signal });The signal also aborts when a graceful shutdown stops waiting for the request. Shutdown then waits for your catch and finally, so clean up there.
The event
useEvent() returns the request's Event, the one object every composable reads from:
request- the incomingRequest.url- the parsed URL.params- the matched route params.ip- the client address, or an empty string when it is unknown. Behind a load balancer, settrustProxyto read the real client's address.response- the mutable response state, covered in shaping the response.context- a per-request object you can extend.
const event = useEvent();
event.request.method // -> 'GET'
event.url.pathname // -> '/search'
event.ip // -> '203.0.113.7'Outside a request there is no event, so useEvent() throws. tryUseEvent() returns undefined instead of throwing. Use it in code that runs with or without a request.
context starts empty, and middleware fills it, for example with an auth session or a resolved user. Augment its type from your app with declare module:
import type { Session } from 'ohnejs/auth';
declare module 'ohnejs' {
interface EventContext {
auth: Session;
}
}