Skip to main content

Request Context

Understand the purpose of the request context object.

Introduction​

Athenna provides an object inside all Http handlers called ctx. This property is implemented by the Context interface imported from @athenna/http package.

The context object​

In Athenna as you can see in the previous documentation page of Middlewares and Controllers we are always destructuring the ctx property and using like this:

Route.get('/welcome', ({ response }) => {
response.status(200).send({ hello: 'world' })
})

But is the same of doing this:

Route.get('/welcome', (ctx) => {
ctx.response.status(200).send({ hello: 'world' })
})

The ctx object is little different for each type of handlers, and we will see all the differences previous in this documentation page.

The request object​

Athenna Request class provides an object-oriented way to interact with the current HTTP request being handled by your application as well as retrieve the ip, headers, body, and files that were submitted with the request.

The id getter​

Get the id from the request:

Route.get('/welcome', ({ request }) => {
console.log(request.id) // 123e4567-e89b-12d3-a456-426614174000

/*....*/
})

This is very useful to trace the requests of your server. Check the tracing requests documentation page for more information.

The ip getter​

Get the ip from where the request were executed:

Route.get('/welcome', ({ request }) => {
console.log(request.ip) // 192.168.0.1

/*....*/
})

The method getter​

Get the REST method of your request:

Route.get('/welcome', ({ request }) => {
console.log(request.method) // GET

/*....*/
})

The hostname, port, protocol and version getters​

Get the hostname used in the request, the port where your server is listening, the protocol (http or https) and the HTTP version of the request:

Route.get('/welcome', ({ request }) => {
console.log(request.hostname) // localhost
console.log(request.port) // 1335
console.log(request.protocol) // http
console.log(request.version) // 1.1

/*....*/
})

The baseUrl, originalUrl and routeUrl getters​

Athenna gives you three different ways to get the url of the request. Let's imagine a request to /users/1?include=profile being handled by the /users/:id route:

Route.get('/users/:id', ({ request }) => {
console.log(request.baseUrl) // /users/1
console.log(request.originalUrl) // /users/1?include=profile
console.log(request.routeUrl) // /users/:id

/*....*/
})
  • baseUrl is the url of the request without the query params.
  • originalUrl is the url exactly as it was requested, with the query params.
  • routeUrl is the url of the route that is handling the request, with the params placeholders. It's very useful for metrics and logs, where you want to group all the requests of the same route.

The baseHostUrl, originalHostUrl and routeHostUrl getters​

The same as the getters above, but concatenating the protocol, host and port of your application:

Route.get('/users/:id', ({ request }) => {
console.log(request.baseHostUrl) // http://localhost:1335/users/1
console.log(request.originalHostUrl) // http://localhost:1335/users/1?include=profile
console.log(request.routeHostUrl) // http://localhost:1335/users/:id

/*....*/
})

The routeName getter​

Get the name of the route that is handling the request. The name is defined using the name() method of your route:

Route.get('/users/:id', ({ request }) => {
console.log(request.routeName) // users.show

/*....*/
}).name('users.show')

The body, params, queries and headers getters​

Retrieve all the data inside each one of then:

Route.post('/welcome/:id', ({ request }) => {
console.log(request.body) // { hello: 'world' }
console.log(request.params) // { id: '1' }
console.log(request.queries) // { world: 'hello' }
console.log(request.headers) // { 'content-type': 'application/json' }

/*....*/
})

The input() and payload() methods​

Retrieve only one value per call from the request body:

Route.post('/welcome/:id', ({ request }) => {
const defaultValue = 'defaultValue'

console.log(request.input('hello', 'found')) // 'world'
console.log(request.input('not-found', defaultValue)) // 'defaultValue'

console.log(request.payload('hello', defaultValue)) // 'world'
console.log(request.payload('not-found', defaultValue)) // 'defaultValue'

/*....*/
})
tip

As you can see, you can use the second argument of this type of methods to set the default value if the key has not been found in your request.

You may even use "dot" syntax to retrieve values that are nested within JSON arrays / objects:

const name = request.input('user.name')

The only() and except() methods​

If you need to retrieve a subset of the input data, you may use the only() and except() methods. Both of these methods accept a single array or a dynamic list of arguments:

const input = request.only('username', 'password')
const input = request.only(['username', 'password'])

const input = request.except('credit_card')
const input = request.except(['credit_card'])
warning

The only() method returns all the key / value pairs that you request; however, it will not return key / value pairs that are not present on the request body.

The param(), query() and header() methods​

Retrieve only one value of params, queries or headers. You can also set a second parameter that will set the default value if the first argument key doesn't exist:

Route.post('/welcome/:id', ({ request }) => {
const defaultValue = 'defaultValue'

console.log(request.param('id', defaultValue)) // '1'
console.log(request.param('not-found', defaultValue)) // 'defaultValue'

console.log(request.query('world', defaultValue)) // 'hello'
console.log(request.query('not-found', defaultValue)) // 'defaultValue'

console.log(request.header('content-type', defaultValue)) // 'application/json'
console.log(request.header('not-found', defaultValue)) // 'defaultValue'

/*....*/
})

The filters() method​

It's very common for list routes to receive pagination, search, filters and ordering in the query string. Instead of parsing all of these values by hand, you can use the filters() method:

// GET /users?page=1&limit=20&search=lenon&where=status:=:active&orderby=-createdAt
Route.get('/users', ({ request }) => {
const filters = request.filters()

console.log(filters)
/*
{
page: 1,
limit: 20,
search: 'lenon',
select: [],
includes: [],
where: [{ field: 'status', op: '=', value: 'active' }],
orderBy: [{ field: 'createdAt', direction: 'DESC' }]
}
*/
})

The object returned is ready to be used with the filter(), search() and paginate() methods of the ORM query builder:

Route.get('/users', async ({ request, response }) => {
const filters = request.filters()

const users = await User.query()
.search(['name', 'email'], filters.search)
.filter(filters)
.paginate({ page: filters.page, limit: filters.limit })

return response.send(users)
})

These are the query string parameters that are parsed:

ParameterExampleDescription
pagepage=1The page to get. Defaults to 0.
limitlimit=20The amount of items per page. Defaults to 10.
searchsearch=lenonA term to search for.
selectselect=id,nameComma separated list of columns to select.
includesincludes=profile,postsComma separated list of relations to load.
wherewhere=status:=:active,age:>=:18Comma separated list of field:op:value filters.
orderbyorderby=-createdAt,nameComma separated list of fields. Use - to order with DESC.

The op of a where filter can be =, !=, >, >=, <, <=, in, not_in, contains, not_contains, between and not_between. The value is automatically converted to null, number or boolean when possible. You can also send arrays using JSON or __ as separator, and quote a value to keep commas inside it:

# Arrays
/users?where=id:in:[1,2,3]
/users?where=age:between:18__30

# Quoted values
/users?where=name:=:"Lenon, João"

# Relationship columns and JSON keys
/users?where=profile.bio:contains:node,metadata->mode:=:dark
Restricting what the client can send​

By default, filters() accepts any field, relation and column sent by the client. This is not safe for most applications, since your users could filter or select columns like password. To avoid that, define what is allowed in your route:

Route.get('/users', ({ request }) => {
const filters = request.filters({
select: ['id', 'name', 'email'],
includes: ['profile'],
where: ['status', 'createdAt', 'profile.bio'],
orderBy: ['createdAt', 'name'],
maxLimit: 50
})

/*....*/
})

When the client sends something that is not allowed, an InvalidFilterException is thrown and a 422 response is sent:

{
"statusCode": 422,
"code": "E_INVALID_FILTER",
"name": "InvalidFilterException",
"message": "The value \"password\" is not allowed in the \"where\" query parameter.",
"help": "Remove \"password\" from the \"where\" query parameter or ask for it to be allowed in this route."
}

If you prefer to silently ignore the values that are not allowed, set the onDenied option to ignore:

const filters = request.filters({
where: ['status'],
onDenied: 'ignore'
})

These are all the options available:

OptionDefaultDescription
selectundefinedThe columns allowed in select. Any column is allowed when not set.
includesundefinedThe relations allowed in includes. Any relation is allowed when not set.
whereundefinedThe fields allowed in where, exactly as sent (profile.bio, metadata->mode). Any field is allowed when not set.
orderByundefinedThe fields allowed in orderby. Any field is allowed when not set.
searchtrueSet as false to always return an empty search term.
maxLimitundefinedThe maximum value of limit. Bigger values are reduced to it.
defaults{ page: 0, limit: 10 }The values used when the client doesn't send a parameter.
onDenied'throw'throw an InvalidFilterException or ignore the value not allowed.

The defaults option is useful to define a default ordering for your list:

const filters = request.filters({
orderBy: ['createdAt', 'name'],
defaults: {
limit: 20,
orderBy: [{ field: 'createdAt', direction: 'DESC' }]
}
})

The file(), files(), parts() and formData() methods​

Retrieve the files sent in multipart/form-data requests. These methods and also the isMultipart(), saveRequestFiles() and cleanRequestFiles() methods are documented in the file uploads documentation page.

The getFastifyRequest() method​

Retrieve the vanilla Fastify request object to use more advanced getters and methods from Fastify:

Route.get('/welcome', ({ request }) => {
const fastifyRequest = request.getFastifyRequest()

/*....*/
})

The response object​

Athenna Response class provides an object-oriented way to interact with the current HTTP response being handled by your application as well set a status code and return the response to the client.

The send() method​

Terminate the request sending a response body to the client:

Route.get('/welcome', ({ response }) => {
response.send({ hello: 'world' })
})

The html() method​

Terminate the request rendering an HTML string in the response body to the client:

Route.get('/welcome', ({ response }) => {
response.html('<h1>Hello World!</h1>')
})

The view() method​

Terminate the request rendering a view in the response body to the client:

Route.get('/welcome', ({ response }) => {
response.view('welcome', { hello: 'world' })
})

The helmet() method​

Apply all the Helmet response headers in your response:

Route.get('/welcome', async ({ response }) => {
if (condition) {
// we apply the default options
await response.helmet()
} else {
// we apply customized options
await response.helmet({ frameguard: false })
}
})

The status() method​

Apply the status code of your response:

Route.get('/welcome', async ({ response }) => {
response.status(200).send({ hello: 'World' })
})

The sendFile() method​

Serve files if the static plugin is enabled in your application:

Route.get('/welcome', async ({ response }) => {
response.status(200).sendFile('img.png')
})

The download() method​

Serve files with a custom name if the static plugin is enabled in your application:

response.status(200).download('img.png', 'custom-img.png')

The header(), safeHeader(), hasHeader() and removeHeader() methods​

Set custom header for your response, the header() method will overwrite the already set headers, the safeHeader() will only register the header if the header is not yet registered, the hasHeader() will verify if the header is already registered and the removeHeader() will remove a header from the response:

Route.get('/welcome', async ({ response }) => {
response.header('content-type', 'application/json')
response.safeHeader('content-type', 'application/json')

if (response.hasHeader('content-type')) {
response.removeHeader('content-type')
}
})

The redirectTo() method​

Redirect your response to another url and with a different status code:

Route.get('/hello', ctx => ctx.response.status(200))

Route.get('/welcome', async ({ response }) => {
response.redirectTo('/hello', 200)
})

The sent getter​

Verify if your response has already been sent to client, useful to be used in interceptors:

Route.get('/welcome', async ({ response }) => {
response.send({ status: 'ok' })
}).interceptor(({ response }) => {
if (response.sent) {
// do something
}
})

The body, statusCode and headers getters​

Get the content of the response body, status code and headers if it exists. These values will be available before you use response.send(), response.status() and response.headers() methods somewhere. These getters are useful when using interceptors and terminators:

Route.get('/welcome', async ({ response }) => {
response.send({ status: 'ok' })
}).terminator(({ response }) => {
if (response.statusCode !== 200) {
// do something
}

if (response.body.status === 'ok') {
// do something
}

if (response.headers['Content-Type'] !== 'application/json') {
// do something
}
})

The responseTime getter​

Get how much time your request has taken until it finish and turn back to client. This value will only be available in terminators:

Route.get('/welcome', async ({ response }) => {
response.send({ status: 'ok' })
}).terminator(({ response }) => {
console.log('Request has taken: ', response.responseTime, 'ms', ' to finish.')
})

The getFastifyResponse() method​

Retrieve the vanilla Fastify response object to use more advanced getters and methods from Fastify:

Route.get('/welcome', ({ response }) => {
const fastifyResponse = response.getFastifyResponse()

/*....*/
})

The data object​

Use the data object to define properties that will be available in your entire request flow. This is really useful for some cases where you want to transfer data from a middleware to a controller for example. Let's see an example setting default pagination values if client has not sent page and limit:

import { Config } from '@athenna/config'
import { Context, Middleware } from '@athenna/http'

@Middleware()
export class PaginationMiddleware {
public async handle({ request, data }: Context) {
const page = request.queries.page ? parseInt(request.queries.page) : 0
const limit = request.queries.limit ? parseInt(request.queries.limit) : 10
const resourceUrl = `${Config.get('http.domain')}${request.baseUrl}`

data.pagination = {
page,
limit,
resourceUrl,
}
}
}

And now is very simple to get this pagination object inside your handler:

Route.get('/products', ({ response, data }) => {
return response.send({ paginationObj: data.pagination })
}).middleware('PaginationMiddleware')
tip

You can also define static values for the data object of a specific route using the data() method of your route.

The context object in middlewares​

Middleware context​

The context of a middleware is the same of a Controller. It uses the same Context interface from @athenna/http package.

Interceptor context​

In interceptors Athenna uses the InterceptContext. This context is quite the same of Context, but it has additional property status.

Terminate middleware context​

In terminators Athenna set the TerminateContext. This context is quite the same of Context, but it has additional properties status and responseTime.