Reply
Middleware can return different types of values to send a response. For simple cases you can return a value directly; for more control, Server.js exports reply helpers that can be used standalone or chained together:
status: set the response's HTTP status code, like201or404.json: stringify the objects and send them asContent-Type: application/json.redirect: send a302(or custom) redirect through theLocationheader.headers: set one or many response headers at once.type: set theContent-Typefrom a MIME type or a file extension likecsv.cache: setCache-Controlfrom a duration like'1h'or a number (seconds).cookies: set or delete response cookies, with options likepathandexpires.send: send any body, auto-detecting text, HTML, JSON, aBufferor a stream.file: stream a file from disk, guessing itsContent-Type,404if missing.download: send a file as an attachment so the browser downloads it.
import { status, json, redirect, ... } from '@server/next';The chainable helpers can be strung together and end with either .send() or a terminal method like .json() or .redirect().
Inline replies
The simplest way to reply is to just return a value from your middleware:
// Plain string → text/plain
// (or text/html if it starts with '<')
.get('/text', () => 'Hello world')
.get('/html', () => '<h1>Hello world</h1>')
// Object or array → application/json
.get('/json', () => ({ hello: 'world' }))
// HTTP status code shorthand (empty body)
.post('/create', () => 201)
// A bare chainable helper is finalized for you (empty body)
.get('/gone', () => status(410))
// Buffer / Blob → raw bytes (a Blob keeps its own type)
.get('/bytes', () => Buffer.from([0x89, 0x50]))
.get('/pdf', () => new Blob([data], { type: 'application/pdf' }))
// A ReadableStream, or a (async) generator, streams chunk by chunk
.get('/stream', async function* () { yield 'a'; yield 'b'; })
// A bucket file handle → streamed back, typed, 404 if missing
.get('/avatar', () => bucket.file('avatars/me.jpg'))
// A web-standard Response object (e.g. from fetch())
.get('/custom', () => new Response('ok', { status: 200 }))status()
Set the HTTP status code. It's chainable, so pair it with a terminal helper like .json() or .send(), or return it on its own for an empty-body response:
import { status, json } from '@server/next';
// Just a status code, empty body
.delete('/users/:id', () => status(204))
// With a JSON body
.post('/users', (ctx) => {
const user = createUser(ctx.body);
return status(201).json(user);
})Reach for status() when you need a body or other helpers; for a bare status code, return 201 is the shorthand.
json()
Stringify a value and send it as Content-Type: application/json. It's a terminal helper, returning the Response.
import { status, json } from '@server/next';
.get('/users', () => json({ users: [] }))
// With a status code
.post('/users', (ctx) => {
return status(201).json({ id: 1, ...ctx.body });
})Returning a plain object also sends JSON; use json() explicitly when you need to chain status() or headers().
redirect()
Redirect the client to another URL. The helper sends a 302 with the Location header; chain status() before it for a different code, like 301, 307 or 308. It's terminal, returning the Response.
import { redirect, status } from '@server/next';
// 302 by default
.get('/old-path', () => redirect('/new-path'))
// Permanent redirect
.get('/moved', () => {
return status(301).redirect('/permanent-path');
})headers()
Set one or more response headers. Pass a key and value, or an object to set several at once. It's chainable, so finish with a terminal helper like .send().
import { headers } from '@server/next';
// A single header
.get('/', () => headers('x-custom', 'value').send('Hello'))
// Several at once
.get('/', () => {
return headers({ 'x-foo': 'bar', 'x-baz': 'qux' })
.send('Hello');
})headers() appends rather than replacing, and a value can be an array to send the same header more than once.
type()
Set the Content-Type header. Accepts a file extension (csv, .csv) or a full MIME type (text/csv). It's chainable.
import { type } from '@server/next';
// By extension
.get('/data.csv', () => type('csv').send('a,b,c'))
// By full MIME type
.get('/data', () => {
return type('application/json').send('{"a":1}');
})Extensions are looked up in the built-in MIME table (the leading dot is optional); an unknown value is used as the content type verbatim. The full table:
| Name | Content-Type |
|---|---|
epub | application/epub+zip |
gz | application/gzip |
jar | application/java-archive |
json | application/json |
jsonld | application/ld+json |
doc | application/msword |
bin | application/octet-stream |
ogx | application/ogg |
pdf | application/pdf |
rtf | application/rtf |
azw | application/vnd.amazon.ebook |
mpkg | application/vnd.apple.installer+xml |
xul | application/vnd.mozilla.xul+xml |
xls | application/vnd.ms-excel |
eot | application/vnd.ms-fontobject |
ppt | application/vnd.ms-powerpoint |
odp | application/vnd.oasis.opendocument.presentation |
ods | application/vnd.oasis.opendocument.spreadsheet |
odt | application/vnd.oasis.opendocument.text |
pptx | application/vnd.openxmlformats-officedocument.presentationml.presentation |
xlsx | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
docx | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
rar | application/vnd.rar |
vsd | application/vnd.visio |
7z | application/x-7z-compressed |
abw | application/x-abiword |
bz | application/x-bzip |
bz2 | application/x-bzip2 |
cda | application/x-cdf |
csh | application/x-csh |
arc | application/x-freearc |
php | application/x-httpd-php |
sh | application/x-sh |
tar | application/x-tar |
xhtml | application/xhtml+xml |
xml | application/xml |
zip | application/zip |
aac | audio/aac |
mid / midi | audio/midi |
mp3 | audio/mpeg |
oga | audio/ogg |
opus | audio/opus |
wav | audio/wav |
weba | audio/webm |
otf | font/otf |
ttf | font/ttf |
woff | font/woff |
woff2 | font/woff2 |
avif | image/avif |
bmp | image/bmp |
gif | image/gif |
jpeg / jpg | image/jpeg |
png | image/png |
svg | image/svg+xml |
tif / tiff | image/tiff |
ico | image/vnd.microsoft.icon |
webp | image/webp |
ics | text/calendar |
css | text/css |
csv | text/csv |
htm / html | text/html |
js / mjs | text/javascript |
md | text/markdown |
text / txt | text/plain |
3gp | video/3gpp |
3g2 | video/3gpp2 |
ts | video/mp2t |
mp4 | video/mp4 |
mpeg | video/mpeg |
ogv | video/ogg |
webm | video/webm |
avi | video/x-msvideo |
cache()
Set the Cache-Control header. Accepts a duration like '1h', a number of seconds, or false/0 for no-store. It sets public, max-age=<seconds> and is chainable. For anything more specific (private, s-maxage, ...), set the header directly with headers().
import { cache, headers } from '@server/next';
// Cache-Control: public, max-age=3600
.get('/posts', () => cache('1h').json(posts))
// Full control: set the header yourself
.get('/me', (ctx) => headers('cache-control', 'private, max-age=300').json(ctx.user))Use this when the value depends on the request; for a fixed default across routes, set the cache option instead. A Cache-Control set here always wins over that default.
cookies()
Set or delete response cookies. Pass a string value, an options object (value, path, expires, and the rest), null to delete, or an object of names to set several. It's chainable.
import { cookies } from '@server/next';
// Simple string value
.post('/login', () => cookies('token', 'abc123').send())
// With options
.post('/login', () => {
return cookies('token', {
value: 'abc123',
path: '/',
expires: '7d',
}).send();
})Delete a cookie by setting it to null, or pass an object to set several at once:
// Delete a cookie
.post('/logout', () => cookies('token', null).send())
// Several at once
.get('/', () => {
return cookies({ theme: 'dark', lang: 'en' }).send();
})expires accepts a duration like '7d' or a Date; deleting sets an already-expired cookie so the browser drops it.
send()
The base terminal method that every chain ends on. Sends the response with an optional body, or an empty one when called with no arguments.
import { send, status } from '@server/next';
.get('/ping', () => send('pong'))
.delete('/item', () => status(204).send())Unless you've already set the Content-Type (via type() or headers()), send() auto-detects it from the body:
stringstarting with<→text/html- other
string→text/plain Buffer→ sent as-isReadableStream→ streamed- anything else →
application/json
Set the type yourself to override this, e.g. to send XML or raw bytes:
import { type } from '@server/next';
// XML string
.get('/feed', () => type('xml').send('<rss>…</rss>'))
// A Buffer (set the type; send() won't guess it)
.get('/report.csv', () => type('csv').send(csvBuffer))
// A ReadableStream is streamed through
.get('/proxy', () => send(upstream.body))send() handles strings, Buffers and ReadableStreams. A Blob or a generator only work as a direct return (see Inline replies), not through send().
file()
Stream a file, from a disk path or a Bucket file handle. A missing file responds with 404.
import { file } from '@server/next';
// A disk path
.get('/', () => file('./index.html'))
.get('/report', () => file('./report.pdf'))
// A stored bucket file (e.g. serving an upload behind auth)
.get('/avatar', () => file(bucket.file('avatars/me.jpg')))file() is async and streams the contents rather than buffering, so it's fine for large files. Return it directly, or chain it after download() to force a download. To serve a stored file behind auth, see Serving stored files.
download()
Send a file as an attachment, so the browser downloads it instead of rendering it (Content-Disposition: attachment). Pass a filename to name the download, then chain .file() for the contents.
import { download } from '@server/next';
// Named download
.get('/export', () => {
return download('export.csv')
.file('./data/export.csv');
})
// Without a filename
.get('/raw', () => {
return download().file('./data/export.csv');
})When the filename has an extension and no Content-Type is set yet, download() also sets the type from that extension.
Chaining examples
The chainable helpers (status, type, headers, cache, cookies, download) return a Reply instance, so you can chain as many as needed before a terminal call (send, json, redirect, file), which returns the Response:
import {
status, headers, cookies, json,
} from '@server/next';
.post('/login', async (ctx) => {
const user = await authenticate(ctx.body);
if (!user)
return status(401).json({
error: 'Invalid credentials',
});
return status(200)
.cookies('session', {
value: user.sessionId, path: '/',
})
.json({ ok: true, user });
})