The basics
HTTP responses
Whatever a route or a controller returns becomes the response: text, JSON, a view, a redirect, a download or a
Response you build yourself.
Creating responses#
Strings and arrays#
A route returns its response. A string is sent as HTML, with a Content-Type of text/:
Route.get('/', () => 'Hello World')A number or a boolean is sent the same way, as its text. An array or a plain object is sent as JSON, with a
Content-Type of application/:
Route.get('/', () => [1, 2, 3])null and undefined send an empty 204 response, so an action that returns nothing answers "No Content".
Response objects#
response() with content builds a Response with the status and the headers you give it. The status is 200
unless you give one, and text is HTML unless you name another Content-Type:
Route.get('/home', () => response('Hello World', 200, { 'Content-Type': 'text/plain' }))Eloquent models and collections#
A model, a collection of models or a paginator is sent as JSON. A model leaves out its hidden attributes, such as
the password and remember_token of the User model:
Route.get('/user/{user}', (user: User) => user)Views#
A view returned from a route is rendered and sent as HTML:
Route.get('/', () => view('welcome', { name: 'Taylor' }))Views covers writing them.
Attaching headers to responses#
Give the headers as response()'s third argument, as an object or as a list of pairs:
const res = response(content, 200, { 'Content-Type': type, 'X-Header-One': 'Header Value', 'X-Header-Two': 'Header Value',})A Response's headers object adds, replaces and removes headers after it is built:
const res = response(content)res.headers.set('X-Header-One', 'Header Value')res.headers.append('Vary', 'Accept')res.headers.delete('X-Debug')On Node, the headers of a response from fetch() or Response.redirect() can't be changed; Bun allows it. A
middleware, which may receive such a response, changes the headers with withHeaders(), which returns a copy whose
headers can be changed on both:
import { withHeaders } from '@orieljs/framework'import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'export class AddFrameOptions implements Middleware { async handle(request: HttpRequest, next: Next): Promise<Response> { const response = await next(request) return withHeaders(response, (headers) => { headers.set('X-Frame-Options', 'DENY') headers.delete('X-Powered-By') }) }}Oriel has no cache.headers middleware: set Cache-Control as any other header.
Attaching cookies to responses#
Queue a cookie with the Cookie facade, and it is added to the response when the response is ready. cookie()
makes the cookie, from its name, its value and the number of minutes it lasts:
Route.get('/', () => { Cookie.queue(cookie('name', 'value', 60)) return 'Hello World'})Queued cookies are added by the AddQueuedCookiesToResponse middleware, in the web group. A route in
routes/ doesn't send them.
A redirect takes a cookie of its own with withCookie(), in any group:
const res = redirect('/dashboard').withCookie('name', 'value', 60)Generating cookie instances#
cookie() returns a plain cookie, which isn't sent until you queue it. Its fourth argument changes the cookie's
path, domain, secure, httpOnly and sameSite:
const theme = cookie('theme', 'dark', 60 * 24 * 30, { httpOnly: false, sameSite: 'strict' })Cookie.queue(theme)A cookie is HttpOnly unless you say otherwise, and takes its path, domain, secure and sameSite from
config/. With 0 minutes, the default, it lasts until the browser closes. Cookie.forever('name', 'value')
makes one that lasts 400 days, and Cookie.make() is the same as cookie().
To write a cookie onto a Response yourself, give its Set-Cookie header the text serializeCookie() makes:
const res = response('Hello World', 200, { 'Set-Cookie': serializeCookie(cookie('name', 'value', 60)) })Expiring cookies early#
Cookie.forget() makes a cookie that has already expired, so the browser deletes it. Queue it:
Cookie.queue(Cookie.forget('name'))Laravel's withoutCookie() and Cookie::expire() aren't in Oriel.
Cookies and encryption#
The EncryptCookies middleware, in the web group, encrypts every cookie the response sets, however it was set:
queued, attached to a redirect or written as a Set-Cookie header. The browser can't read or change them, and
request().cookie() decrypts them on the next request (see
Requests). A cookie that deletes one, as Cookie.forget() makes, is
sent as it is.
To send some cookies unencrypted, such as a cookie your scripts read, list their names with
EncryptCookies.except() in bootstrap/, as the starter kits do for the appearance cookie:
import { dirname } from 'node:path'import { Application, EncryptCookies } from '@orieljs/framework'import providers from './providers'export function createApplication(): Promise<Application> { return Application.configure({ basePath: dirname(import.meta.dirname) }) .withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/up' }) .withMiddleware(() => { EncryptCookies.except(['cookie_name']) }) .withProviders(providers) .create()}Such a cookie is also read as the browser sent it. Laravel's $middleware->encryptCookies(except: [...]) is
EncryptCookies.except([...]) in Oriel.
The api group has no EncryptCookies: an API route's cookies are sent and read as they are.
Redirects#
redirect() with a path returns a redirect response, a 302 to that path on the host the request came to. A second
argument changes the status:
Route.get('/dashboard', () => redirect('/home/dashboard'))Route.get('/old', () => redirect('/new', 301))back() redirects to the previous page, such as when a submitted form is invalid. It reads the request's Referer
header, then the last page the session recorded, and falls back to the path you give it, or /:
Route.post('/user/profile', () => { // Validate the request... return back().withInput()})The session records each GET page a user visits through the web group, leaving out AJAX requests and prefetches.
Redirecting to named routes#
redirect() with no arguments returns the redirector, whose route() redirects to a named route:
const res = redirect().route('login')A route's parameters are the second argument, and the status the third:
// For a route with the following URI: /profile/{id}const res = redirect().route('profile', { id: 1 })Once route:types has run, TypeScript checks the route's name and its parameters, as it does for route(): see
typed route parameters. Laravel's redirect()->action(), which redirects to a
controller action, isn't in Oriel: name the route and redirect to its name.
Populating parameters via Eloquent models#
A model fills a parameter with its route key, its id unless you change it:
// For a route with the following URI: /profile/{id}const res = redirect().route('profile', user)A {id:slug} parameter, or getRouteKeyName() on the model, fills it from another column: see
customizing the key.
Redirecting to external domains#
redirect() sends a URL that starts with http://, https://, //, #, mailto:, tel: or sms: as it is,
and treats anything else as a path of your app. away() sends any target as it is:
const res = redirect().away('https://www.google.com')Redirecting with flashed session data#
A redirect usually comes with a message for the next page, flashed to the session. with()
flashes a value, or an object of values, as it redirects:
Route.post('/user/profile', () => { // ... return redirect('/dashboard').with('status', 'Profile updated!')})The next page reads it from the session. In a TSX view:
import { session } from '@orieljs/framework'export default function Dashboard() { const status = session<string | undefined>('status') return <main>{status && <div class="alert alert-success">{status}</div>}</main>}Redirecting with input#
withInput() flashes the request's input, so the form can show it again after a failed validation. old() reads
it on the next request (see Requests):
const res = back().withInput()It takes the input to flash, too: withInput(request().except('password')).
Redirecting with errors#
withErrors() flashes error messages, by field, for errors() to read on the next page. A field takes one message
or a list:
const res = back().withErrors({ email: 'These credentials do not match our records.' }).withInput()A second argument names an error bag, so two forms on one page keep their errors apart:
withErrors({ email: '…' }, 'login'), read with errors('login'). A failed validation does all of
this for you.
A redirect takes headers too: redirect('/. redirect().refresh() redirects to the
current URL. redirect().guest(path) remembers the page a guest was going to before it redirects, and
redirect().intended(fallback, status) later sends the user there; redirect().setIntendedUrl(url) sets that page
yourself. The Inertia starter kits' sign-in uses intended(), and
Authentication covers the flow.
Other response types#
response() with no arguments returns the response factory, whose methods build the other kinds of response.
View responses#
To send a view with your own status or headers, use the factory's view(). It renders the view and returns a
promise of the response:
Route.get('/hello', () => response().view('hello', data, 200, { 'Content-Type': type }))Without a status or headers of your own, return the view() itself.
JSON responses#
json() turns its data into JSON, with a Content-Type of application/. A status and headers follow:
const res = response().json({ name: 'Abigail', state: 'CA' })const created = response().json({ name: 'Abigail' }, 201)The data goes through JSON.stringify(), so models and collections become JSON as they do when a route returns
them, and dates become ISO strings. Laravel's JSONP withCallback() isn't in Oriel.
Empty responses#
noContent() sends an empty 204 response, or another status you give it:
const res = response().noContent()File downloads#
download() sends a file for the browser to save. A second argument is the name the user sees, and a third the
headers:
Route.get('/report', () => response().download(pathToFile))Route.get('/report/named', () => response().download(pathToFile, name, headers))The file is streamed from disk, with its size, and a Content-Type from its name's extension unless your headers
give one. A name that isn't ASCII is sent too, so Résumé.pdf arrives as it is. A file that doesn't exist is a 404.
download() returns a promise: return it from the route, or await it.
File responses#
file() sends a file for the browser to show, such as an image or a PDF, rather than to download. It streams the
file as download() does, with a Content-Type from its extension unless your headers give one:
Route.get('/invoice', () => response().file(pathToFile))Route.get('/invoice/raw', () => response().file(pathToFile, headers))Streamed responses#
stream() sends a web ReadableStream, chunk by chunk, as your code produces them. The client can start reading
before the response ends:
Route.get('/stream', () => { const encoder = new TextEncoder() const body = new ReadableStream<Uint8Array>({ async start(controller) { for (const text of ['developer', 'admin']) { controller.enqueue(encoder.encode(text)) await new Promise((resolve) => setTimeout(resolve, 2000)) // Simulate delay between chunks... } controller.close() }, }) return response().stream(body, 200, { 'X-Accel-Buffering': 'no' })})Streamed downloads#
A stream becomes a download with a Content-Disposition header. contentDisposition() writes it, with the name
encoded as download() encodes it:
const res = response().stream(body, 200, { 'Content-Type': 'text/markdown', 'Content-Disposition': contentDisposition('attachment', 'readme.md'),})contentDisposition('inline', 'invoice.pdf') asks the browser to show the file instead. Laravel's streamJson()
and eventStream() aren't in Oriel.
Response macros#
Oriel has no response macros. A response you reuse is a function of your own that returns a Response.