There are many libraries helping you validate that an email is an email and a postal code is a postal code. That's great, we need those too.
But first an foremost, as API creators, we want to provide the consumers of our APIs with predictable and human readable error messages:
{
"status": 400,
"message": "Bad request",
"errors": {
"firstName": {
"type": "missing",
"message": "expected string, got undefined"
},
"lastName": {
"type": "invalid",
"message": "expected string, got number"
},
"devices": [
{
"index": 0,
"type": "missing",
"message": "expected object, got undefined"
},
{
"index": 2,
"notificationPriority": {
"type": "invalid",
"message": "expected number, got string"
}
}
],
"skills": {
"canCook": {
"type": "missing",
"message": "expected boolean, got undefined"
}
},
"weakApi": {
"type": "invalid",
"message": "expected string || number || shape, got object"
}
}
}
Before diving into the nitty gritty, we want to be able to simply tackle 95% of the bad requests, without breaking a sweat.
We only want to make sure that an email is a valid email if we have an otherwise complete payload. ie: All the required fields are populated, and whatever data we get is of the expected type.
What this library offers is a simple way to create complex validation logic on an API route
in a Node.js app.
It responds automatically to a Bad request
with a 400
status code and detailed feedback.
Most importantly, embracing middleware philosophy, it allows you to craft route handlers
that can safely assume
they will only ever be called with a valid payload
.
You can always call additional middleware to validate the incoming payload further if you need to. The advantage of using this library higher up the middleware stack, is that middleware functions below it can focus on more granular validation details, without having to worry about the presence of a value or its type. (ie: your email validator no longer needs to check if a value is a non-empty string, it can strictly focus on the more relevant logic that makes an email an email)
Here is how it would look like if you built your API with Express:
- Create a custom validator:
// foodieValidator.js
const { createValidator, type } = require('typemania').express;
const foodieValidator = createValidator(type.shape({
firstName: type.string.isRequired,
age: type.number,
visitedRestaurants: type.arrayOf(type.shape({
name: type.string.isRequired,
address: type.object,
liked: type.boolean.isRequired,
})).isRequired,
favoriteRestaurants: type.arrayOf(type.string),
noIdea: type.arrayOf(type.oneOf([ type.string, type.number ]))
}));
module.exports = foodieValidator;
- Use it like you would any middleware:
// server.js
const express = require('express');
const foodieValidator = require('./path/to/foodieValidator.js');
const router = express.Router();
router.post('/foodnerds', foodieValidator, (req, res) => {
// fearless destructuring happens here
res.send('yum');
});
Largely inspired by the Proptypes
library from the React
ecosystem...
just, reduced to the bare minimum to describe a JSON
payload.
const { type } = require('typemania');
// optional string
type.string
// required string
type.string.isRequired
Validate the received value is an object literal of any
shape:
type.object
type.object.isRequired
Validate the received value is an object literal of a specific
shape:
// optional object with required property "age" and optional property "isYoung"
type.shape({
age: type.number.isRequired,
isYoung: type.boolean
})
Complex objects:
// required object with nested objects
type.shape({
firstName: type.string.isRequired,
address: type.shape({
main: type.string,
postalCode: type.string.isRequired,
}),
vagueOtherThings: type.object,
}).isRequired
Validate the received value is an array of elements of any
type:
type.array
type.array.isRequired
Validate the received value is an array of elements of a specific
type:
// [ '',, null]
type.arrayOf(type.string) // allow null and undefined values
// [ '', '']
type.arrayOf(type.string.isRequired) // strictly strings
Complex arrays:
// required array of arrays [[], [], []]
type.arrayOf(type.array).isRequired
// optional array of arrays of objects
// [
// [{ nested: [{}], arrays: [] }],
// [{ nested: [{}], arrays: [] }, { nested: [{}], arrays: [] }]
// ]
type.arrayOf(type.arrayOf(type.shape({
nested: type.arrayOf(type.object),
arrays: type.array,
})))
// Either a string or a number
type.oneOf([type.string, type.number])
Complex array example with oneOf:
// [
// { "prop1": "val1" },
// 5,
// true,
// "if that's really what you want..."
// ]
type.arrayOf(type.oneOf([
type.shape({ prop1: type.string }),
type.number,
type.boolean,
type.string,
]));
If you need to guarantee the type of an element at a specific index within the array, you might want to reconsider your API specs first.
The library does not offer a type.any
as it would go against its design philosophy.
But if you must, there are ways...
The library does not offer a type.exact
. Instead of forcing the consumers of your API
to provide an exact value, consider setting defaults in your API code.
The single concern
of this library is a value's type
, not the value itself.
It cannot help you validate that a value is either a 5 or an 8, solely that it is indeed a number.
I'm actually interested, but I don't use Express
As long as the framework you work with has a clearly defined middleware signature, you can make your own validators generator with createFrameworkValidator
.
Express middleware functions have the following signature: (req, res, next) => {}
So here is how createExpressValidator
is made under the hood:
const createExpressValidator = (typeDefinitions) => (req, res, next) => createFrameworkValidator({
validate: typeDefinitions,
payload: req.body,
handleBadRequest: (error) => res.status(error.status).send(error),
handleValidRequest: next,
});
This means to create your own makeValidator
you will write something like this:
const { createFrameworkValidator } = require('typemania');
/**
* @param {*} typeDefinitions ex: type.shape({ firstName: type.string }).isRequired
* @returns {Function} A middleware function
*/
const makeValidator = (typeDefinitions) => {
// here "todo" is a placeholder
// for whatever the signature of a middleware function is
// in the framework that you work with
return function middleware(todo) {
return createFrameworkValidator({
validate: typeDefinitions,
payload: todo, // retrieve the payload from the request the way your framework dictates
handleBadRequest: (error) => {
// const { status, message, errors } = error;
todo; // send response and end the request the way your framework dictates
}, // called if payload is invalid
handleValidRequest: todo, // the next middleware or route handler to be called if a payload is valid
});
};
};
module.exports = makeValidator;
Which you would use like:
const makeValidator = require('./path/to/makeValidator.js');
const myThingValidator = makeValidator(type.shape({
myProp1: type.string,
myProp2: type.number,
etc: type.arrayOf(type.object)
}));
// and then just plug the myThingValidator middleware in the routes that need gatekeeping
// the way your framework dictates