Joi validation is powerful and easy to work with, however it’s not always obvious or easy to add stuff like error code(s) to the hapijs response. This post will show you a way (or two) to deal with that problem.
Step 1, assign error codes to each validation error Quick example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 server.route ({ method : 'POST' , path : '/person' , options : { validate : { payload : { firstName : Joi .string () .min (5 ) .max (10 ) .required() .error (errors => { errors.forEach (err => { switch (err.type ) { case 'any.empty' : case 'any.required' : err.message = 'Firstname should not be empty!' err.context = { errorCode : 111 } break case 'string.min' : err.message = `Firstname should have at least ${ err.context.limit } characters!` err.context = { errorCode : 121 } break case 'string.max' : err.message = `Firstname should have at most ${ err.context.limit } characters!` err.context = { errorCode : 131 } break default : break } }) return errors }) } } }, handler : async request => { console .log ('to save' , request.payload ) return { result : 'ok' } } })
Step 2, customize failAction when creating Hapi server 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const Boom = require ('@hapi/boom' )const server = Hapi .Server ({ routes : { validate : { failAction : (request, h, err ) => { const firstError = err.details [0 ] if (firstError.context .errorCode !== undefined ) { throw Boom .badRequest (err.message , { errorCode : firstError.context .errorCode }) } else { throw Boom .badRequest (err.message ) } } } } })
Step 3, customize response The reason this step is needed is because Hapi would strip out the injected errorCode
attribute created by step 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 server.ext ('onPreResponse' , (request, h ) => { const response = request.response if (!response.isBoom ) { return h.continue } const { data } = response if (data !== undefined ) { response.output .payload = { ...response.output .payload , ...data } } return h.continue })
With the above setup, request
1 curl http://localhost:3000/person -d ''
would result in
1 { "statusCode" : 400 , "error" : "Bad Request" , "message" : "child \"firstName\" fails because [Firstname should not be empty!]" , "errorCode" : 111 }
By default abortEarly: true
is set Hapi, if multiple error codes are desired, only Step 2 needs to be adjusted to
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const server = Hapi .Server ({ routes : { validate : { options : { abortEarly : false }, failAction : (request, h, err ) => { const errorCodes = err.details .map (e => e.context .errorCode ) .filter (e => e !== undefined ) throw Boom .badRequest (err.message , { errorCodes }) } } } })
request
1 curl http://localhost:3000/person -d 'firstName='
would return
1 { "statusCode" : 400 , "error" : "Bad Request" , "message" : "child \"firstName\" fails because [Firstname should not be empty!, Firstname should have at least 5 characters!]" , "errorCodes" : [ 111 , 121 ] }
If you need to add error code in your application code, you can simply achieve that by returning a Boom
like the following
1 return Boom .badRequest ('Your error message here' , { errorCode : YOUR_CODE })
I’ve composed a gist in case you want to save some typings in trying out the code. Cheers!