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
/*....*/
})
baseUrlis the url of the request without the query params.originalUrlis the url exactly as it was requested, with the query params.routeUrlis 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'
/*....*/
})
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'])
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:
| Parameter | Example | Description |
|---|---|---|
page | page=1 | The page to get. Defaults to 0. |
limit | limit=20 | The amount of items per page. Defaults to 10. |
search | search=lenon | A term to search for. |
select | select=id,name | Comma separated list of columns to select. |
includes | includes=profile,posts | Comma separated list of relations to load. |
where | where=status:=:active,age:>=:18 | Comma separated list of field:op:value filters. |
orderby | orderby=-createdAt,name | Comma 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:
| Option | Default | Description |
|---|---|---|
select | undefined | The columns allowed in select. Any column is allowed when not set. |
includes | undefined | The relations allowed in includes. Any relation is allowed when not set. |
where | undefined | The fields allowed in where, exactly as sent (profile.bio, metadata->mode). Any field is allowed when not set. |
orderBy | undefined | The fields allowed in orderby. Any field is allowed when not set. |
search | true | Set as false to always return an empty search term. |
maxLimit | undefined | The 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')
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.