Response
This guide covers the AdonisJS Response class and the methods available to construct HTTP responses. You will learn about:
- Sending response bodies in different formats
- Working with headers and cookies
- Handling redirects and file downloads
- Understanding how the response serialization works
- Extending the Response class with custom methods
Overview
The Response class provides helpers for constructing HTTP responses in AdonisJS applications. Instead of working directly with Node.js's raw response object, the Response class offers a fluent, expressive API for common tasks like sending JSON, setting headers, handling redirects, and streaming file downloads.
The Response class is available via the ctx.response property. You can access it in route handlers, middleware, and exception handlers throughout your application. For many simple responses, you can return values directly from route handlers, and AdonisJS will automatically use the Response class to send them.
Sending response body
The Response class provides multiple ways to send response bodies. You can either return values directly from route handlers or use explicit response methods.
Returning values from route handlers
The simplest approach is to return values directly from your route handler. AdonisJS will automatically serialize the value and set appropriate content headers.
import router from '@adonisjs/core/services/router'
router.get('/', async () => {
/**
* Returns plain text with content-type: text/plain
*/
return 'This is the homepage.'
})
router.get('/welcome', async () => {
/**
* Returns HTML fragment with content-type: text/html
*/
return '<p>This is the homepage</p>'
})
router.get('/api/page', async () => {
/**
* Returns JSON with content-type: application/json
*/
return { page: 'home' }
})
router.get('/timestamp', async () => {
/**
* Date instances are converted to ISO strings
*/
return new Date()
})
Using response.send()
You can also explicitly use the response.send() method, which provides the same automatic content-type detection.
import router from '@adonisjs/core/services/router'
router.get('/', async ({ response }) => {
/**
* send() method works identically to returning values.
* Useful when you need to set headers or status before sending.
*/
response.send('This is the homepage')
})
router.get('/data', async ({ response }) => {
/**
* Objects and arrays are automatically stringified
*/
response.send({ page: 'home' })
})
Forcing JSON responses
When you need to ensure the response is sent as JSON (even if it might be detected as HTML), use the response.json() method.
See also: Response body serialization
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async index({ response }: HttpContext) {
const posts = await Post.all()
/**
* Explicitly sets content-type to application/json
* and serializes the posts array
*/
response.json(posts)
}
}
Working with headers
The Response class provides methods for setting, appending, and removing HTTP headers. Headers must be set before the response body is sent.
Setting headers
Use the response.header() method to set a response header. If the header already exists, it will be overridden.
import type { HttpContext } from '@adonisjs/core/http'
export default class ApiController {
async index({ response }: HttpContext) {
/**
* Set custom header for API versioning
*/
response.header('X-API-Version', 'v1')
/**
* Set cache control headers
*/
response.header('Cache-Control', 'public, max-age=3600')
return { status: 'ok' }
}
}
Setting headers safely
The response.safeHeader() method sets a header only if it doesn't already exist. This is useful when you want to provide a default value without overriding existing headers.
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
export default class CorsMiddleware {
async handle({ response }: HttpContext, next: NextFn) {
/**
* Set CORS header only if not already set by another middleware
*/
response.safeHeader('Access-Control-Allow-Origin', '*')
await next()
}
}
Appending to headers
Some headers can have multiple values. Use response.append() to add additional values without removing existing ones.
import type { HttpContext } from '@adonisjs/core/http'
export default class DownloadsController {
async show({ response }: HttpContext) {
/**
* Append multiple Set-Cookie headers for different cookies
*/
response.append('Set-Cookie', 'session=abc123; HttpOnly')
response.append('Set-Cookie', 'preferences=dark-mode; Path=/')
return { download: 'ready' }
}
}
Removing headers
Remove a previously set header using response.removeHeader().
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
export default class SecurityMiddleware {
async handle({ response }: HttpContext, next: NextFn) {
await next()
/**
* Remove server header to hide server implementation details
*/
response.removeHeader('X-Powered-By')
}
}
Handling redirects
The response.redirect() method returns an instance of the Redirect class, which provides a fluent API for creating redirect responses with different destinations and options.
Redirecting to a path
Use response.redirect().toPath() to redirect to a specific URI or external URL.
import type { HttpContext } from '@adonisjs/core/http'
export default class AuthController {
async logout({ response, auth }: HttpContext) {
await auth.logout()
/**
* Redirect to the home page after logout
*/
response.redirect().toPath('/')
}
async external({ response }: HttpContext) {
/**
* Redirect to an external website
*/
response.redirect().toPath('https://adonisjs.com')
}
}
Redirecting to a named route
The response.redirect().toRoute() method accepts a route identifier and its parameters, making redirects maintainable when URLs change.
import Post from '#models/post'
import { createPostValidator } from '#validators/post'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async store({ request, response }: HttpContext) {
/**
* Validate the incoming request data
*/
const payload = await request.validateUsing(createPostValidator)
/**
* Create the post
*/
const post = await Post.create(payload)
/**
* Redirect to the show page for the newly created post
*/
response.redirect().toRoute('posts.show', [post.id])
}
}
Redirecting back
Use response.redirect().back() to redirect to the previous page. This uses the referrer header and falls back to / if the referrer is not available.
import type { HttpContext } from '@adonisjs/core/http'
export default class CommentsController {
async destroy({ response, params }: HttpContext) {
/**
* Redirect back to the page the user came from
*/
response.redirect().back()
}
}
Setting redirect status code
By default, redirects use a 302 status code. Use the status() method to set a different redirect status.
import type { HttpContext } from '@adonisjs/core/http'
export default class PagesController {
async oldPage({ response }: HttpContext) {
/**
* Permanent redirect (301) for moved pages
*/
response.redirect().status(301).toPath('/new-page')
}
}
Forwarding query strings
Use the withQs() method to forward the current URL's query string to the redirect destination.
import type { HttpContext } from '@adonisjs/core/http'
export default class SearchController {
async filter({ response }: HttpContext) {
/**
* Forward existing query parameters to the new URL.
* If the current URL is /search?q=adonis&sort=date,
* this redirects to /results?q=adonis&sort=date
*/
response.redirect().withQs().toPath('/results')
}
}
Setting custom query strings
Pass an object to withQs() to set custom query parameters. Chain the method multiple times to append additional parameters.
import type { HttpContext } from '@adonisjs/core/http'
export default class ProductsController {
async search({ response }: HttpContext) {
/**
* Redirect with custom query parameters.
* Results in /products?category=electronics&sort=price
*/
response
.redirect()
.withQs({ category: 'electronics' })
.withQs({ sort: 'price' })
.toPath('/products')
}
}
Streaming and downloads
The Response class provides methods for streaming data and serving file downloads. These methods handle proper headers, caching, and cleanup automatically.
Streaming responses
Use response.stream() to send a readable stream as the response. AdonisJS waits for the route handler and upstream middleware to finish before beginning to read from the stream.
import { createReadStream } from 'node:fs'
import type { HttpContext } from '@adonisjs/core/http'
export default class ExportsController {
async generate({ response }: HttpContext) {
/**
* Create a readable stream from a file
*/
const stream = createReadStream('./exports/data.csv')
/**
* Stream the file to the client.
* AdonisJS handles backpressure and cleanup automatically.
*/
response.stream(stream)
}
}
Downloading files
The response.download() method streams a file and sets appropriate headers for downloads. It accepts an absolute path to the file.
import app from '@adonisjs/core/services/app'
import type { HttpContext } from '@adonisjs/core/http'
export default class InvoicesController {
async download({ response, params }: HttpContext) {
/**
* Construct absolute path to the invoice file
*/
const filePath = app.makePath(`storage/invoices/${params.id}.pdf`)
/**
* Stream the file for download with ETag support.
* Browser caches the response as long as file contents remain unchanged.
*/
response.download(filePath)
}
}
Force download with custom filename
Use response.attachment() to force a download and specify a custom filename. The browser will prompt the user to save the file with the given name.
import app from '@adonisjs/core/services/app'
import type { HttpContext } from '@adonisjs/core/http'
export default class ReportsController {
async export({ response, params }: HttpContext) {
/**
* Absolute path to the report file
*/
const filePath = app.makePath(`storage/reports/${params.id}.xlsx`)
/**
* Force download with a user-friendly filename.
* The second parameter sets the filename in Content-Disposition header.
*/
response.attachment(filePath, `monthly-report-${params.month}.xlsx`)
}
}
Setting response status
The Response class provides methods for setting HTTP status codes. Status codes communicate the result of the request to the client.
Setting status code
Use response.status() to set the HTTP status code. This method overrides any previously set status code.
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async store({ request, response }: HttpContext) {
const post = await Post.create(request.all())
/**
* Return 201 Created for successful resource creation
*/
response.status(201)
return post
}
}
Setting status safely
The response.safeStatus() method sets a status code only if one hasn't been set already. This is useful in middleware where you want to provide a default without overriding explicit status codes.
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
export default class JsonApiMiddleware {
async handle({ response }: HttpContext, next: NextFn) {
await next()
/**
* Set default success status only if not already set.
* Controller-specific status codes take precedence.
*/
response.safeStatus(200)
}
}
Status shorthand methods
AdonisJS provides shorthand methods that set both the status code and response body in one call.
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'
export default class UsersController {
async show({ response, params }: HttpContext) {
const user = await User.find(params.id)
if (!user) {
/**
* Sets status 404 and sends response body in one call
*/
return response.notFound({ error: 'User not found' })
}
return user
}
async destroy({ response, params, auth }: HttpContext) {
const user = await User.findOrFail(params.id)
if (user.id !== auth.user.id) {
/**
* Sets status 403 and sends error message
*/
return response.forbidden({ error: 'Cannot delete other users' })
}
await user.delete()
return response.noContent()
}
}
Response shorthand methods
AdonisJS provides shorthand methods for common HTTP status codes. Each method sets the status code and optionally sends a response body.
| Method | Status Code |
|---|---|
response.continue() | 100 |
response.switchingProtocols() | 101 |
response.ok(body?, generateEtag?) | 200 |
response.created(body?, generateEtag?) | 201 |
response.accepted(body?, generateEtag?) | 202 |
response.nonAuthoritativeInformation(body?, generateEtag?) | 203 |
response.noContent() | 204 |
response.resetContent() | 205 |
response.partialContent(body?, generateEtag?) | 206 |
response.multipleChoices(body?, generateEtag?) | 300 |
response.movedPermanently(body?, generateEtag?) | 301 |
response.movedTemporarily(body?, generateEtag?) | 302 |
response.seeOther(body?, generateEtag?) | 303 |
response.notModified(body?, generateEtag?) | 304 |
response.useProxy(body?, generateEtag?) | 305 |
response.temporaryRedirect(body?, generateEtag?) | 307 |
response.badRequest(body?, generateEtag?) | 400 |
response.unauthorized(body?, generateEtag?) | 401 |
response.paymentRequired(body?, generateEtag?) | 402 |
response.forbidden(body?, generateEtag?) | 403 |
response.notFound(body?, generateEtag?) | 404 |
response.methodNotAllowed(body?, generateEtag?) | 405 |
response.notAcceptable(body?, generateEtag?) | 406 |
response.proxyAuthenticationRequired(body?, generateEtag?) | 407 |
response.requestTimeout(body?, generateEtag?) | 408 |
response.conflict(body?, generateEtag?) | 409 |
response.gone(body?, generateEtag?) | 410 |
response.lengthRequired(body?, generateEtag?) | 411 |
response.preconditionFailed(body?, generateEtag?) | 412 |
response.requestEntityTooLarge(body?, generateEtag?) | 413 |
response.requestUriTooLong(body?, generateEtag?) | 414 |
response.unsupportedMediaType(body?, generateEtag?) | 415 |
response.requestedRangeNotSatisfiable(body?, generateEtag?) | 416 |
response.expectationFailed(body?, generateEtag?) | 417 |
response.unprocessableEntity(body?, generateEtag?) | 422 |
response.tooManyRequests(body?, generateEtag?) | 429 |
response.internalServerError(body?, generateEtag?) | 500 |
response.notImplemented(body?, generateEtag?) | 501 |
response.badGateway(body?, generateEtag?) | 502 |
response.serviceUnavailable(body?, generateEtag?) | 503 |
response.gatewayTimeout(body?, generateEtag?) | 504 |
response.httpVersionNotSupported(body?, generateEtag?) | 505 |
Working with cookies
The Response class provides methods for setting cookies with different security levels. AdonisJS supports signed cookies, encrypted cookies, and plain cookies.
Setting signed cookies
The response.cookie() method creates a signed cookie. Signed cookies cannot be tamprered but their content is visible to the client.
import type { HttpContext } from '@adonisjs/core/http'
export default class PreferencesController {
async update({ response, request }: HttpContext) {
const theme = request.input('theme')
/**
* Set a signed cookie that expires in 2 hours.
* The signature prevents client-side tampering.
*/
response.cookie('theme', theme, {
maxAge: '2h'
})
return { success: true }
}
}
Setting encrypted cookies
Use response.encryptedCookie() to encrypt cookie values. The app.appKey encrypts the value, making it unreadable to clients. If the app key changes, the cookie cannot be decrypted.
import type { HttpContext } from '@adonisjs/core/http'
export default class SessionsController {
async create({ response, auth }: HttpContext) {
/**
* Store sensitive session data in an encrypted cookie.
* The value is encrypted using app.appKey.
*/
response.encryptedCookie('session_data', {
userId: auth.user.id,
loginAt: new Date()
})
return { success: true }
}
}
Setting plain cookies
The response.plainCookie() method creates a base64-encoded cookie without signing or encryption.
import type { HttpContext } from '@adonisjs/core/http'
export default class TrackingController {
async view({ response }: HttpContext) {
/**
* Set a plain cookie for non-sensitive tracking data
*/
response.plainCookie('last_visit', new Date().toISOString())
return { success: true }
}
}
Supported cookie value types
Cookie values can be any of the following data types: string, number, bigInt, boolean, null, object, and array.
import type { HttpContext } from '@adonisjs/core/http'
export default class CartController {
async add({ response, request }: HttpContext) {
const cartItems = [
{ id: 1, name: 'Product A', quantity: 2 },
{ id: 2, name: 'Product B', quantity: 1 }
]
/**
* Arrays and objects are automatically serialized
*/
response.encryptedCookie('cart', cartItems)
return { success: true }
}
}
Cookie options
All cookie methods accept an options object to control cookie behavior. These options match the configuration in config/app.ts under the http.cookie block.
import type { HttpContext } from '@adonisjs/core/http'
export default class AuthController {
async login({ response, auth }: HttpContext) {
/**
* Set authentication cookie with security options
*/
response.encryptedCookie('auth_token', auth.token, {
domain: '',
path: '/',
maxAge: '2h',
httpOnly: true, // Prevent JavaScript access
secure: true, // Only send over HTTPS
sameSite: 'lax', // CSRF protection
partitioned: false, // Experimental: CHIPS
priority: 'medium' // Experimental: Cookie priority
})
return { success: true }
}
}
domain
Cookie domain. Empty string means current domain.
path
Cookie path. Default is /.
maxAge
Cookie expiry time. Can be a duration string like '2h' or milliseconds.
httpOnly
Prevent JavaScript access to the cookie.
secure
Only send cookie over HTTPS connections.
sameSite
CSRF protection. Default is 'lax'.
partitioned
Experimental: Enable cookie partitioning (CHIPS).
priority
Experimental: Cookie priority hint for browsers.
Running actions after response has been sent
The response.onFinish() method allows you to register callbacks that execute after the response has been sent to the client. This is useful for cleanup tasks, logging, or operations that shouldn't block the response.
Cleaning up after file downloads
A common use case is deleting temporary files after they've been streamed to the client.
import { unlink } from 'node:fs/promises'
import app from '@adonisjs/core/services/app'
import type { HttpContext } from '@adonisjs/core/http'
export default class FilesController {
async download({ response, params }: HttpContext) {
const filePath = app.makePath(`tmp/exports/${params.id}.zip`)
/**
* Register cleanup callback before streaming the file
*/
response.onFinish(async () => {
await unlink(filePath)
})
/**
* Stream the file. After the download completes,
* the cleanup callback will run automatically.
*/
response.download(filePath)
}
}
Logging response metrics
You can use onFinish() to log analytics or metrics without delaying the response.
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
export default class AnalyticsMiddleware {
async handle({ response, request }: HttpContext, next: NextFn) {
const startTime = Date.now()
/**
* Register analytics logging after response is sent.
* This doesn't delay the response to the client.
*/
response.onFinish(() => {
const duration = Date.now() - startTime
console.log(`${request.method()} ${request.url()} - ${duration}ms`)
})
await next()
}
}
Response body serialization
The Response class automatically serializes response bodies and sets appropriate content-type headers based on the data type you send.
When you send a response, AdonisJS performs two operations: it serializes the value into a string format suitable for HTTP transmission, and it sets the appropriate content-type and content-length headers.
| Data Type | Serialization Behavior | Content-Type |
|---|---|---|
| Arrays and objects | Stringified using safe stringify (like JSON.stringify() but removes circular references and serializes BigInt) | application/json |
HTML fragments (strings starting with <) | Sent as-is | text/html |
| Numbers and booleans | Converted to string | text/plain |
| Date instances | Converted to ISO string using toISOString() | text/plain |
| Regular expressions | Converted to string using toString() | text/plain |
| Error objects | Converted to string using toString() | text/plain |
| JSONP responses | Stringified appropriately | text/javascript |
| Other plain strings | Sent as-is | text/plain |
| Other data types | Results in an exception | - |
This automatic handling means you rarely need to manually set content-type headers.
import router from '@adonisjs/core/services/router'
router.get('/api/data', async () => {
/**
* Returns JSON with content-type: application/json
* BigInt values are safely serialized as strings
*/
return {
id: BigInt(9007199254740991),
timestamp: new Date(),
active: true
}
})
router.get('/page', async () => {
/**
* Returns HTML with content-type: text/html
*/
return '<h1>Welcome</h1>'
})
router.get('/text', async () => {
/**
* Returns plain text with content-type: text/plain
*/
return 'Hello, world!'
})
Extending Response class
You can add custom methods and properties to the Response class using macros and getters. This allows you to create reusable response helpers that match your application's needs.
Read the Extending AdonisJS guide if you are new to the concept of macros and getters.
Adding custom methods
Use Response.macro() to add methods to all Response instances throughout your application.
import { Response } from '@adonisjs/core/http'
export default class AppProvider {
async boot() {
/**
* Add a custom method to send API responses with consistent structure
*/
Response.macro('api', function (data: any, meta?: Record<string, any>) {
return this.json({
success: true,
data: data,
meta: meta || {}
})
})
/**
* Add a custom method for paginated responses
*/
Response.macro('paginated', function (items: any[], pagination: any) {
return this.json({
data: items,
pagination: {
page: pagination.page,
perPage: pagination.perPage,
total: pagination.total
}
})
})
}
}
Augmenting the Response class type
After adding custom methods, you must inform TypeScript about them by augmenting the Response interface. Otherwise, you will get type errors when trying to use the custom methods.
import { Response } from '@adonisjs/core/http'
declare module '@adonisjs/core/http' {
export interface Response {
/**
* Send a standardized API response
*/
api(data: any, meta?: Record<string, any>): void
/**
* Send a paginated response
*/
paginated(items: any[], pagination: {
page: number
perPage: number
total: number
}): void
}
}
Using custom methods
Once defined, your custom methods are available on all Response instances.
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async index({ response }: HttpContext) {
const posts = await Post.all()
/**
* Use the custom api() method for consistent responses
*/
return response.api(posts, {
version: 'v1',
timestamp: new Date()
})
}
async paginated({ response, request }: HttpContext) {
const page = request.input('page', 1)
const posts = await Post.query().paginate(page, 20)
/**
* Use the custom paginated() method
*/
return response.paginated(posts.all(), posts.getMeta())
}
}