We’re big fans of Serverless functions at Torii, and we’re always looking for ways to increase the robustness and our productivity.
One of the problems we face with Lambda functions is making sure invocations are passed the correct inputs. As we’re also (happy) users of Hapi and Joi, we decided to borrow the input validation idea and apply it to our Lambda functions.
Example validation
Here’s a simple Lambda function that calculates a full name based on first and last names:
export const handler = async (e, context) => {
const { firstName, lastName } = e
return [firstName, lastName].filter(Boolean).join(' ')
}
We’d like to make sure that firstName and lastName are always sent in the event e and that those are valid strings.
In order to validate the input with Joi, we’ve written a utility function to wrap our handler, handlerWrapper and this is how we use it:
import Joi from '@hapi/joi'
import { handlerWrapper } from './handlerWrapper'
export const handler = handlerWrapper({
validation: {
e: {
firstName: Joi.string().min(3).max(20).required(),
lastName: Joi.string().min(3).max(20).required()
}
},
handler: async (e, context) => {
const { firstName, lastName } = e
return [firstName, lastName].filter(Boolean).join(' ')
}
})
We pass the handlerWrapper two arguments:
- The validation schema, powered by Joi
- The Lambda handler, which is the same as before
The event e will be validated based on the Joi schema provided and will throw an exception if it did not pass validation. Keeping your function safe and exposing invalid invocations.
Testing
Let’s invoke the function using the Serverless Framework with some valid and invalid inputs:
serverless invoke -f fullname --data '{ "firstName": "Steve" }'
// Throws an exception
// Error: "lastName" is required
And another:
serverless invoke -f fullname --data '{ "firstName": "St", lastName: "Jobs" }'
// Throws an exception
// Error "firstName" length must be at least 3 characters long
And a valid input:
serverless invoke -f fullname --data '{ "firstName": "Steve", lastName: "Jobs" }'
RESULT:
"Steve Jobs"
The code
This is the code for handlerwrapper.js:
import Joi from '@hapi/joi'
export const handlerWrapper = (options) => {
const { validate = {}, handler } = validateAndThrow(options, schema)
return async (e, ctx) => {
const eventWithDefaults = validateAndThrow(e, validate.e || {})
return handler(eventWithDefaults, ctx)
}
}
const validateAndThrow = (e, schema) => {
const { result, isValid, errors } = validateEvent(e, schema)
if (!isValid) {
throw new Error(errors.join(', '))
}
return result
}
const validateEvent = (e, schema) => {
const result = Joi.validate(e, schema)
const isValid = (result.error === null)
const errors = isValid ? null : result.error.details.map(detail => detail.message)
return { result: result.value, isValid, errors }
}
const schema = {
validate: Joi.object({
e: Joi.object()
}),
handler: Joi.func().maxArity(2)
}
Other ideas?
If you have other ideas for making Lambda functions more robust, please share with me in the comments or over twitter at ketacode.
Keeping your Lambda functions safe with Joi was originally published in The Startup on Medium, where people are continuing the conversation by highlighting and responding to this story.