Custom responses implementation


#1

I’m coming from sailsjs framework… After seeing Adonis, it blew my mind :slight_smile:

There was one really nice feature in sailsjs that I would like to mimic…

Sails had custom responses logic (https://sailsjs.com/documentation/concepts/custom-responses). So with small amount of customization you could have code like this in controllers:

if(!params.something) return res.badRequest()
if(!user) return res.notFound()
if(somethingHappened) return res.myCustomResponseLogic()

My first try to introduce something like this was by adding hooks:

const {hooks} = require('@adonisjs/ignitor')

hooks.after.providersBooted(() => {

  const Response = use('Adonis/Src/Response')
  const formatResponse = use('App/Helpers/FormatResponse')

  const responses = [
    {status: 200, name: 'ok'},
    {status: 400, name: 'badRequest'},
    {status: 401, name: 'unauthorized'},
    {status: 403, name: 'forbidden'},
    {status: 404, name: 'notFound'}
  ]

  responses.forEach((res) => {

    Response.macro(res.name, function (data, overrideStatus) {
      this.status(overrideStatus || res.status).json(formatResponse(data, {defaultMsg: res.name}))
    })
  })
})

With this code, I achieved similar functionality but I can’t introduce any logic inside my macro depending on request that was made… For example, I would add some custom logic to one of the responses if request.input(‘something’) was true…

This could also be solved by calling my custom response like response.someMacroResponse(data, request) but I strongy belive there is a better and prettier way of achieving this…

Any ideas guys :slight_smile: ?


#2

Why not add something like this:

responses.forEach(res => {
  Response.macro(res.name, function (data, overrideStatus) {
    if (typeof overrideStatus === 'function') {
      return overrideStatus()
    }

    this.status(overrideStatus || res.status).send(
      blueprint(data, null, null)
    )
  })
})

Then in a controller method you might do something like this:

response.badRequest(data, () => {
  if (request.input('data') !== 'foo') {
    response.status(201).send({ data: request.input('data') })
  } else {
    response.status(400).send({ data: 'bar' })
  }
})

#3

However,

IMO, the solution can tend to look very messy in a controller. :smile:

Let’s take the 2 of the 3 examples that you provided and try use AdonisJS to handle the errors.

You might try a validator then respond with the response object:

let something = request.input('something')
const validation = await validate(something, {
  something: 'required'
})

if (validation.fails()) {
  return response.badReqeust()
}

response.ok()

You could try to add an exception handler to hooks.js

Exception.handle('ModelNotFoundException', async (error, { response }) => {
  response.notFound(error.name, { model: error.message })
})

Hope this help :smiley:


#4

Hey! :wave:t3:

Adonis already provides a convenient way to send different responses.

They are defined here: https://github.com/poppinss/node-res/blob/develop/methods.js

It means you can call

response.ok()
response.nonAuthoritativeInformation()
// ...

You can centralise where you are handling your exception by creating an exception handler (see the doc). Note that this feature is going to change in 4.1 to be easier to use (see this issue).


#5

@romain.lanz whao! this is great news!

This will definetely help… However, I still have one question… In sails, response file was last way out… Is there any way to format the end response in single location in adonis?

I hope I exaplained it well :slight_smile:


#6

Sorry, but I didn’t get what you mean.


#7

Where is the final response.send in adonis?

Is it possible to create handler for formating data just before it goes to client?

For example… I want any response (error, array, object, string, w/e) to be formatted as json like this:

{
"data": whatever it was sent to response.XXX(),
"message": "..."
}

#8

Create a middleware and push it as the first middleware in the stack of global middleware.

class ResponseFormatMiddleware {
  async handle (ctx, next) {
    await next()

    const existingResponse = ctx.response._lazyBody.content
    ctx.response.send(newResponse)
  }
}

I am personally against this approach, since it creates a blind abstraction which changes the response output, without giving a hint where it is getting changed.

In a nutshell, your app will have 3 kinds of responses. Exceptions, Validation Errors and Successful response.

  1. Successful response must be created in controller and should never be modified.

  2. There is a global exceptions handler, where you can handle and format all exception responses. This way you can also log/report exceptions to tools like bugsnag.

  3. For validations, I recommend using Validators and you can define a global error format for all validations.

This all seems like too much work but keeps your code in a way that others can understand and no magic code is getting executed before ending the response.


#9

@virk Yep, middleware will do the trick… Can’t belive I didnt figure this one out :slight_smile: Thank you again!

I agree with you that this is not a best approach, but for my team this will be really good… We used sailsjs for long time as API only with “magic” response formatter, so we just want to keep logic that worked nicely.

This allows us to never fail response from API to mobile app which sometimes crashes if they get wrong json keys etc, no mather what happens.

In background we are using exception and validation error logic, but our json formatter at the end shoots nice payload which is also translated depending on few rules in it… For API only it really speeds up the process

EDIT: @virk OMG! when I implemented this solution I realized I can do downstream and upstream middleware behaviour just like in koa.js Love adonis even more now :heartpulse:


#10

Hi guys…

Since @adonisjs/framework updated from 4.0.31 to 5.0.0 there is one change that is breaking some of my code implementations…

On version 4.0.31 when I was calling response.anyCustomMethod(…) it would set method inside response._lazyBody.method to anyCustomMethod… So for example… Calling response.ok(…) I would get response._lazyBody.method === ‘ok’ and so on… Now it always equals “send”.

Is this beahviour intended?

@virk is there any way of fetching which response method was called inside controller now? Or I should implement my custom method that fetches method by status code?


#11

5.0.0 is part of major release. I suggest not using it unless docs and upgrade guide is out.