For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/routing/responses.md.

Responses

A handler returns its response. It never writes to the native response object: Dotpress builds the response, on every adapter the same way.

Return a value

The handler returnsResponse
a value (object, array, string, number…)200 JSON, or the route status
nothing (undefined)204 No Content, or the route status
an error (notFoundError()…)the error, in the standard format
a response helperthe response it describes
defineRoute<"/jobs", EmptyObject, false, unknown[]>(route: RouteConfig<"/jobs", EmptyObject, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.method?: HttpMethod | undefined

Defaults to get.

method
: 'post',
RouteConfig<"/jobs", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/jobs"
path
: '/jobs',
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.status?: number | undefined

Success status. Defaults to 200 (204 when nothing is returned).

status
: 202,
RouteConfig<"/jobs", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {}, false, unknown>
handler
: () =>
const jobs: {
    enqueue(name: string): Promise<{
        id: string;
    }>;
    cancel(id: string): Promise<void>;
}
jobs
.
function enqueue(name: string): Promise<{
    id: string;
}>
enqueue
('export'),
})
defineRoute<"/jobs/:id", EmptyObject, false, unknown[]>(route: RouteConfig<"/jobs/:id", EmptyObject, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.method?: HttpMethod | undefined

Defaults to get.

method
: 'delete',
RouteConfig<"/jobs/:id", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/jobs/:id"
path
: '/jobs/:id',
RouteConfig<"/jobs/:id", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {
    id: string;
}, false, unknown>
handler
: async ({
req: RouteRequest<EmptyObject, {
    id: string;
}, false>
req
}) => {
await
const jobs: {
    enqueue(name: string): Promise<{
        id: string;
    }>;
    cancel(id: string): Promise<void>;
}
jobs
.
function cancel(id: string): Promise<void>
cancel
(
req: RouteRequest<EmptyObject, {
    id: string;
}, false>
req
.
DotpressRequest<{ id: string; }, QueryValues, unknown, UploadedFiles>.params: {
    id: string;
}
params
.
id: string
id
)
}, })

The first route answers 202 with the job as JSON; the second, 204 without body.

Status, headers and cookies

ctx.res sets the metadata of the response, whatever the handler returns:

defineRoute<"/sessions", EmptyObject, false, unknown[]>(route: RouteConfig<"/sessions", EmptyObject, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.method?: HttpMethod | undefined

Defaults to get.

method
: 'post',
RouteConfig<"/sessions", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/sessions"
path
: '/sessions',
RouteConfig<"/sessions", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {}, false, unknown>
handler
: async ({
res: DotpressResponse
res
}) => {
const
const session: {
    id: string;
    token: string;
}
session
= await
const createSession: () => Promise<{
    id: string;
    token: string;
}>
createSession
()
res: DotpressResponse
res
.
DotpressResponse.setStatus(status: number): DotpressResponse
setStatus
(201)
.
DotpressResponse.setHeader(name: string, value: string | string[]): DotpressResponse
setHeader
('cache-control', 'no-store')
.
DotpressResponse.setCookie(name: string, value: string, options?: CookieOptions): DotpressResponse
setCookie
('session',
const session: {
    id: string;
    token: string;
}
session
.
token: string
token
, {
CookieOptions.httpOnly?: boolean | undefined
httpOnly
: true,
CookieOptions.secure?: boolean | undefined
secure
: true,
CookieOptions.sameSite?: "strict" | "lax" | "none" | undefined
sameSite
: 'lax',
CookieOptions.maxAge?: number | undefined

Lifetime in seconds.

maxAge
: 60 * 60 * 24,
}) return {
id: string
id
:
const session: {
    id: string;
    token: string;
}
session
.
id: string
id
}
}, })
MethodEffect
setStatus(status)Status of the response
setHeader(name, value)Sets a header, replacing its value
appendHeader(name, value)Adds a value to a header
removeHeader(name)Removes a header
setCookie(name, value, options)Adds a Set-Cookie header
clearCookie(name, options)Expires a cookie: use the path and domain it was set with

Headers and cookies are kept on error responses returned or thrown as HttpError, and dropped on unexpected errors (500).

Cookie security

setCookie() only applies the options you give, with path: '/' by default. For a session or any secret, set httpOnly: true (out of reach of the JavaScript of the page), secure: true (HTTPS only) and sameSite. For authentication, prefer the cookie mode of @dotpress/auth, which sets them for you.

Request cookies are parsed: ctx.req.cookies.session.

Response helpers

For anything other than plain JSON, return a helper from @dotpress/core:

return json(data, { status: 201, headers: { location: '/members/1' } })
return text('OK')
return redirect('/login') // 302; or 301, 303, 307, 308
return file(bytes, { filename: 'invoice.pdf', contentType: 'application/pdf' })
return stream(source, { contentType: 'text/csv' })
return sse(source)
return empty({ status: 202 })
HelperResponse
json(data, init)JSON with a status and headers. Still validated by the response schema and filtered.
text(body, init)text/plain; charset=utf-8, or the content-type of init.headers (HTML, CSV…)
redirect(location, status)A redirect, 302 by default
file(body, init)A download: Uint8Array, ArrayBuffer, Blob or ReadableStream, with a filename and disposition (attachment or inline)
stream(source, init)A streamed body
sse(source, init)Server-Sent Events
empty(init)No body: 204 by default

Responses other than json() skip the response schema and the response filters.

Redirects are not prefixed with the base path of the app: use ctx.publicPath('/login') to redirect to a route of the app behind a proxy.

Streams

stream() sends the body as it is produced. The source is a ReadableStream, an iterable, an async iterable, or a function returning one: typically an async generator, which receives the abort signal of the request.

defineRoute<"/orders/export", EmptyObject, false, unknown[]>(route: RouteConfig<"/orders/export", EmptyObject, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<"/orders/export", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/orders/export"
path
: '/orders/export',
RouteConfig<"/orders/export", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {}, false, unknown>
handler
: () =>
function stream(source: StreamSource<StreamChunk> | StreamChunk, init?: StreamInit): StreamResult

Streamed response. The source can be a ReadableStream, an iterable, an async iterable, or a function returning one (typically an async generator function, which receives the request abort signal). Chunks are strings (encoded as UTF-8) or Uint8Array instances.

@examplestream (async function* ({ signal }) { for await (const row of db.export({ signal })) yield ${row.csv}\n }, { contentType: 'text/csv' })
stream
(
async function* ({
signal: AbortSignal

Aborted when the client disconnects (same signal as ctx.req.signal).

signal
}) {
yield 'id,total\n' for await (const
const order: {
    id: string;
    total: number;
}
order
of
const orders: {
    export(options: {
        signal: AbortSignal;
    }): AsyncIterable<{
        id: string;
        total: number;
    }>;
}
orders
.
function export(options: {
    signal: AbortSignal;
}): AsyncIterable<{
    id: string;
    total: number;
}>
export
({
signal: AbortSignal
signal
})) {
yield `${
const order: {
    id: string;
    total: number;
}
order
.
id: string
id
},${
const order: {
    id: string;
    total: number;
}
order
.
total: number
total
}\n`
} }, {
StreamInit.contentType?: string | undefined

Defaults to application/octet-stream.

contentType
: 'text/csv' }
), })

When the client disconnects, the signal is aborted and the generator stops: its finally blocks run, so the database cursor is released.

Server-Sent Events

sse() sends events to the browser, which reads them with EventSource. Each event is a string, or an object with data (sent as JSON when it is not a string), event, id, retry or comment:

defineRoute<"/notifications/live", EmptyObject, false, unknown[]>(route: RouteConfig<"/notifications/live", EmptyObject, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<"/notifications/live", EmptyObject, EmptyObject, false, unknown[], unknown>.path: "/notifications/live"
path
: '/notifications/live',
RouteConfig<"/notifications/live", EmptyObject, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<EmptyObject, {}, false, unknown>
handler
: () =>
function sse(source: StreamSource<SseMessage>, init?: SseInit): SseResult

Server-Sent Events response. The source yields events ({ data, event, id, retry, comment }) or strings sent as data. Non-string data is serialized as JSON.

Source functions receive the request abort signal: use it to stop waiting for new events when the client disconnects.

@examplesse (async function* ({ signal }) { for await (const order of orders.watch({ signal })) { yield { event: 'order', id: order.id, data: order } } })
sse
(async function* ({
signal: AbortSignal

Aborted when the client disconnects (same signal as ctx.req.signal).

signal
}) {
for await (const
const notification: {
    id: string;
    text: string;
}
notification
of
const notifications: {
    watch(options: {
        signal: AbortSignal;
    }): AsyncIterable<{
        id: string;
        text: string;
    }>;
}
notifications
.
function watch(options: {
    signal: AbortSignal;
}): AsyncIterable<{
    id: string;
    text: string;
}>
watch
({
signal: AbortSignal
signal
})) {
yield {
SseEvent.event?: string | undefined

Event type (message on the client when omitted).

event
: 'notification',
SseEvent.id?: string | number | undefined

Event identifier, sent back by the client as Last-Event-ID.

id
:
const notification: {
    id: string;
    text: string;
}
notification
.
id: string
id
,
SseEvent.data?: unknown

Event payload. Strings are sent as is, other values as JSON.

data
:
const notification: {
    id: string;
    text: string;
}
notification
}
} }), })

A comment is sent every 15 seconds to keep the connection open through proxies (heartbeat option, false to disable it).

An error raised by a stream after the response has started is logged and reported to onError, and the connection is closed: the status was already sent.

Client disconnections

ctx.req.signal is aborted when the client goes away. Pass it to long operations, so that they stop instead of working for nobody:

handler: async ({ req }) => {
  const report = await fetch(reportServiceUrl, { signal: req.signal })
  return report.json()
}
Coming from v1

Handlers no longer receive the Express res: return the value, and use ctx.res for the status, headers and cookies. Returning nothing now always answers 204.