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.

start/routes.ts
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.

start/routes.ts
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

app/controllers/posts_controller.ts
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.

app/controllers/api_controller.ts
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.

app/middleware/cors_middleware.ts
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.

app/controllers/downloads_controller.ts
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().

app/middleware/security_middleware.ts
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.

app/controllers/auth_controller.ts
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.

app/controllers/posts_controller.ts
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.

app/controllers/comments_controller.ts
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.

app/controllers/pages_controller.ts
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.

app/controllers/search_controller.ts
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.

app/controllers/products_controller.ts
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.

app/controllers/exports_controller.ts
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.

app/controllers/invoices_controller.ts
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.

app/controllers/reports_controller.ts
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.

app/controllers/api/posts_controller.ts
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.

app/middleware/json_api_middleware.ts
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.

app/controllers/users_controller.ts
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.

MethodStatus 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.

app/controllers/preferences_controller.ts
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.

app/controllers/sessions_controller.ts
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.

app/controllers/tracking_controller.ts
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 }
  }
}

Cookie values can be any of the following data types: string, number, bigInt, boolean, null, object, and array.

app/controllers/cart_controller.ts
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 }
  }
}

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.

app/controllers/auth_controller.ts
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
string

Cookie domain. Empty string means current domain.

path
string

Cookie path. Default is /.

maxAge
string | number

Cookie expiry time. Can be a duration string like '2h' or milliseconds.

httpOnly
boolean

Prevent JavaScript access to the cookie.

secure
boolean

Only send cookie over HTTPS connections.

sameSite
'lax' | 'strict' | 'none'

CSRF protection. Default is 'lax'.

partitioned
boolean

Experimental: Enable cookie partitioning (CHIPS).

priority
'low' | 'medium' | 'high'

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.

app/controllers/files_controller.ts
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.

app/middleware/analytics_middleware.ts
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 TypeSerialization BehaviorContent-Type
Arrays and objectsStringified using safe stringify (like JSON.stringify() but removes circular references and serializes BigInt)application/json
HTML fragments (strings starting with <)Sent as-istext/html
Numbers and booleansConverted to stringtext/plain
Date instancesConverted to ISO string using toISOString()text/plain
Regular expressionsConverted to string using toString()text/plain
Error objectsConverted to string using toString()text/plain
JSONP responsesStringified appropriatelytext/javascript
Other plain stringsSent as-istext/plain
Other data typesResults in an exception-

This automatic handling means you rarely need to manually set content-type headers.

start/routes.ts
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.

Note

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.

providers/app_provider.ts
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.

types/response.ts
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.

app/controllers/posts_controller.ts
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())
  }
}