Connect-like middleware to validate/transform data.
This library helps you more comfortable to get along with writing express validation/transformation middlewares, be more confident to write your business logic without worrying about the data's validity.
- Ensure a value exists.
const express = require('express')
const {transformer} = require('express-transformer')
const app = express()
app.post('/login',
transformer('username').exists(),
transformer('password').exists(),
(req, res, next) => {
//req.body.age and req.body.password must exist, for sure
})
app.listen(3000)
- Check passwords be the same.
const express = require('express')
const {transformer} = require('express-transformer')
const app = express()
app.post('/signup',
transformer('username').exists(),
transformer('password').exists(),
transformer(['password', 'passwordConfirm']).transform(([password, passwordConfirm]) => {
if (password !== passwordConfirm) throw new Error('Passwords do not match')
}, {validateOnly: true}),
(req, res, next) => {
//req.body.age and req.body.password exist
//and, req.body.password === req.body.passwordConfirm
})
app.listen(3000)
- Convert the 1-base
page
string parameter in the query to a 0-base number.
app.get('/article',
transformer('page', {location: 'query'})
.defaultValue(1)
.toInt({min: 1})
.transform(val => val - 1),
(req, res, next) => {}
)
- Check password length and validate email format, a more complicated, but obviously common case.
app.post('/signup',
transformer('email')
.exists()
.message('Please provide email')
.isEmail()
.message('Unrecognized email')
.transform(async email => { // transformer function can be async
if (await User.findByEmail(email).exec()) throw new Error('Email already existed')
}, {validateOnly: true}),
transformer(['password', 'passwordConfirm'], {validateOnly: true})
.exists()
.trim()
.isLength({min: 8})
.message('Password is too short')
.transform(([password, passwordConfirm]) => {
if (password !== passwordConfirm) throw new Error('Passwords do not match')
}, {validateOnly: true}),
(req, res, next) => {}
)
- Convert an id to a user object.
app.get('/user/:id',
transformer('id', {location: 'params'})
.matches(/^[a-f\d]{24}$/i)
.message('Invalid user id format')
.transform(async id => {
const user = await User.findById(id).exec()
if (user) throw new Error('Incorrect id')
return user //req.params.id will become the `user` object
})
.message(async id => { // the message function can also be async
return `${id} is not a valid user id`
}),
(req, res) => {res.status(200).json(req.params.id.toJSON())}
)
- Check token.
app.get('/admin/update',
transformer('token')
.exists()
.is('secret-value')
.message('Invalid credential', {global: true}),
(req, res) => {}
)
- Deep array iteration.
transformer('user.messages[].stars[]').toInt({min: 0, max: 5})
- Array of arrays iteration.
transformer(['me.favorites[]', 'posts[].meta.comments[].likes[]', 'credits'])
.transform(([favorite, like, credits]) => {
// perform the check here
// when validateOnly is true, the returned value will be ignored
}, {validateOnly: true})
- Force the input data shape.
app.get('/products/set-categories',
transformer('products[].config.categories[]')
.transform(() => void 0, {validateOnly: true, force: true}),
(req, res) => {
// Setting force = true ensures that the following for-loop will
// NEVER throw any error with ANY (malformed) input data.
for (const {config: {categories}} of req.body.products) {
for (const category of categories) {
console.log(category)
}
}
}
)
- Without
force
, the transformation chain only fixes if the input contains a malformed (non array, non object) value.
app.get('/products/set-categories',
transformer('products[].config.categories[]')
.transform(() => void 0, {validateOnly: true}), // why is 'void 0'? Because I am too lazy to type 'undefined'.
(req, res) => {
// The following for-loop will NEVER throw any error with ANY (malformed) input data.
//req.body.products, if exists, will be ensured to be in array type
for (const {config: {categories}} of req.body.products || []) {
// categories, if exists, will be ensured to be in array type
for (const category of categories || []) {
console.log(category)
}
}
}
)
- This library is built with a strict security concern in mind,
it should work in almost any condition with almost any malformed input data
It also ensures you the data format (when
force
is true, or array of paths is used with at least one element exists).
If you find this is not correct, please fire a bug issue.
Once the input data passes all transformations/validations, you can freely use the value specified by path at any depth level.
For example, with transformer('review.stars').exists().toInt()
, in all handlers following after,
you can freely use req.body.review.stars
without worrying about what the input was initially.
For instance, if the input was req = {body: {review: 'foo'}}
, calling req.body.review.stars
without checking will
throw an error, and may break your app.
However, the transformation automatically detects the pattern you require and changes the value for you.
- Another malformed input data pattern.
Consider the input with the data req = {body: {reviews: {0: {stars: 7}}}}
, and the transformer transformer('reviews[].stars').exists()
,
Without the transformer, there will be no error when accessing req.body.reviews[0].stars
.
However, because array notation is specified after the reviews
key, the input is reset to an empty array, and becomes {reviews: []}
.
- Conditional transformation/validation.
app.post(
'/order/:id',
(req, res, next) => {
// if returned is true, reason must be provided
if (req.body.returned) transformer('reason').exists()(req, res, next)
else next()
}
)
- Conditioning with multiple transformations.
const {combineMiddlewares} = require('middleware-async')
app.post(
'/order/:id',
transformer('action').exists().isIn(['review', 'return']),
(req, res, next) => combineMiddlewares(
req.body.action === 'review'
? [
transformer('stars').exists(),
transformer('id').transform(idToOrder),
transformer('comment')
.exists()
.message((_, {req}) => `Plesae provide comment on your review on the order of "${req.params.id.product.name}", created at ${req.params.id.createdAt.toLocaleString()}`)
]
: transformer('reason').exists()
)(req, res, next)
)
- Add your own custom method via plugins.
const {addTransformerPlugin} = require('express-transformer')
addTransformerPlugin({
name: 'isPostalCode',
getConfig() {
return {
transform(value, info) {
if (typeof value !== 'string') throw new Error(`${info.path} must be a string`)
if (!/^\d{3}-\d{4}$/.test(value)) throw new Error(`${info.path} is a valid postal code`)
},
options: {
validateOnly: true
}
}
}
})
app.post(
'/change-address',
transformer('postalCode').exists().isPostalCode()
)
- Extend the Typescript typing for plugin.
declare global {
namespace ExpressTransformer {
export interface ITransformer<T, V, Options> {
isPostalCode(): ITransformer<T, string, Options>
}
}
}
- Another more comprehensive plugin example, for batch operation, with options.
const {addTransformerPlugin, TransformationError} = require('express-transformer')
addTransformerPlugin({
name: 'allExist',
getConfig(options) {
return {
transform(values, info) {
if (values.some(value => value === null || value === undefined || (!options.acceptEmptyString && value === ''))) {
throw new TransformationError(`All paths (${info.path.join(', ')}) must be provided`, info)
}
},
options: {
force: true,
validateOnly: true
}
}
}
})
app.post(
'/signup',
transformer(['username', 'password', 'first', 'last']).allExist({acceptEmptyString: false})
)
- Combine multiple transformations by plugins' names.
app.post(
'/update',
transformer('first').use([
['exists'],
['isType', 'string'],
['isLength', {max: 50}],
['message', 'invalid first name']
]),
transformer('postalCode').use([
['exists'],
['isType', 'string'],
['isPostalCode'], // this plugin must be added before
['message', 'Invalid postal code']
]),
)
- Combine multiple transformations using the plugin objects.
const {isType} = requrie('express-transformer')
const isPostalCode = {
name: 'isPostalCode',
getConfig() {
return {
transform(value, info) {
if (typeof value !== 'string') throw new Error(`{info.path} must be a string`)
if (!/^\d{3}-\d{4}$/.test(value)) throw new Error(`${info.path} is a valid postal code`)
},
options: {
validateOnly: true
}
}
}
}
app.post(
'/update',
transformer('postalCode').use([
['exists'],
[isType, 'string'],
[isPostalCode],
[
'transform',
async (postalCode, {req}) => {
// the req object is available during the transformation
(req.locals ||= {}).address = await postalToAddress(postalCode)
},
{validateOnly: true}
],
])
)
All default plugins are exported and available to be used in this way (or you can use their names, either).
const {
transform,
exists,
isIn,
isEmail,
isLength,
matches,
message,
toDate,
toFloat,
toInt,
trim,
defaultValue,
is,
isArray,
isType,
use
} = require('express-transformer')
- Interestingly,
.use
itself is a plugin.
transformer('page', {location: 'param'}).use([
['defaultValue', 1]
['use', [['toInt', {min: 1}], ['transform', page => page - 1]]]
])
- By this way, you can save the configuration for reuses.
const requiredString = len => [
['exists'],
['message', (_, {path}) => `${path} is required`]
['isType', 'string'],
['message', 'invalid input type']
['isLength', {max: len}],
['message', (_, {path}) => `${path} must be at most ${len} characters`]
]
app.post('/update',
transformer('first').use(requiredString(50)),
transformer('last').use(requiredString(50)),
transformer('introduction').use(requiredString(256)),
)
- Another alternative way to combine multiple transformations.
const chainTransformations = (path, chains) => chains.reduce(
(chain, [name, ...params]) => chain[name](...params),
transformer(path)
)
app.post('/update', chainTransformations('firstName', [
['exists', {acceptEmptyString: false}],
['message', 'Please enter your first name'],
['isType', 'string'],
['message', 'First name must be a string'],
['isLength', {max: 50}],
['message', 'First name is too long'],
['transform', value => value.toUpperCase()]
]))
- Access various information during the transformation.
Note: the comments in the code are for general cases.
app.post(
'/update',
transformer('postalCode')
.exists()
.isType('string')
.matches(/^\d{3}-\d{4}$/)
.transform(async (
postalCode, // value or array of value
{
req, // the req object
path, // string or array of strings
pathSplits, // array of strings or array of array of strings
options: {
location, // string
rawPath, // (optional) boolean
rawLocation, // (optional) boolean
disableArrayNotation // (optional) boolean
}
}
) => {
// the req object is available during the transformation
(req.locals ||= {}).address = await postalToAddress(postalCode)
}, {validateOnly: true})
)
- And more ready-to-use validators/transformers, namely:
.exists()
.is(value)
.isArray()
.isEmail()
.isIn(list)
.isLength()
(check string length or array's length).isType(type)
.matches(regex)
.defaultValue(value)
.toDate()
.toFloat()
.toInt()
.trim()
.use()
(combine multiple transformations)
Plugins with extendable Typescript typing can be configured to add new methods permanently.
- What will happen if your path contains an array notation or the dot character, such as when you want to check
req.body['first.name'']
?
No worry, the disabelArrayNotation
, rawPath
, and rawLocation
options are there for you.
Please check the API references section below.
The library exports the following methods.
transformer
(also exported asdefault
)addTransformerPlugin
Typically, you only need to use the transformer()
method in most of the cases.
This method returns a transformation chain that is used to validate/transform the input data.
The transformation chain is also a connect-like middleware, and can be placed in the handler parameter in express (e.g. app.use(chain)
).
A transformation/validation can be appended to a transformation chain by calling chain.<method>
.
Internally, the library does not define any method directly.
Instead, it adds methods by plugins via calling addTransformerPlugin
.
For example: .message
, .transform
, .isEmail
, .exists
, .defaultValue
, .toInt
, ...
Check the Plugins section below for more information.
All methods in the transformation chain always return the chain itself.
It is possible and highly recommended to add transformations to the chain in the chaining style.
For example: chain.exists().isEmail().transform(emailToUser).message('Email not found')
Of course, it is possible to use the non-chaining style. For instance:
const chain = transformer('page', {location: 'query'})
chain.defaultValue('1').toInt()
chain.transform(v => v + 1)
app.use('/articles', chain, (req, res) => {})
transformer: (path, transformerOptions) => chain
- Parameters:
-
(required) path: string | string[]
: a universal-path formatted string or an array of universal-path formatted string.Sample values:
'email'
,'foo[]'
,['foo', 'bar']
,['foo[]', 'foo[].bar.baar[]', 'fooo']
. -
(optional) transformerOptions: Object
: an object with the following properties.(optional) location: string (default: 'body')
: a universal-path formatted string, which specifies where to find the input value from thereq
object.(optional) rawLocation: boolean (default: false)
: treat thelocation
value as a single key (i.e., do not expand to a deeper level).(optional) rawPath: boolean (default: false)
: treat path the exact key without the expansion.(optional) disableArrayNotation: boolean (default: false)
: disable array iteration (i.e., consider the array notation as part of the key name).
-
- Returned value: a connect-like middleware that inherits all methods from the transformation chain's prototype.
All methods in a transformation chain are defined by plugins. Here are two most basic ones for most of your needs.
You can add your own method via addTransformerPlugin
.
The Typescript typing is also available to be extended.
chain.transform(callback, options)
: append a custom transformation/validation to the chain.-
Parameters:
(required) callback: (value, info) => any | Promise<any>
: the callback of the transformation/validation. The callback can be an async or a normal function, which should accept the following parameters.value
: value or array of values- If the
path
parameter intransformer(path, transformerOptions)
is a string,value
will be the value of the current input. - If the
path
parameter intransformer(path, transformerOptions)
is an array of string,value
will be the array of the values of the list of current inputs.
- If the
info: Object
: an object which includes the following properties.-
path
: the path to the current input () or the array of paths to the current list of inputs. -
pathSplits
: an array that contains a string (object key) or number (array index) values used to determine the traversal path. When thepath
parameter is an array, this value will be an array of array. -
req
: the requestreq
object from the connect-like middleware. -
options: Object
: thetransformerOptions
object passed in.transform(callback, transformerOptions)
with default fields (i.e., thelocation
field) filled.Note: this
options
object is always a non-null object, withoptions.location
reflects the currently being used value of thelocation
config. Which means, for e.g., iftransformerOptions
isundefined
,options
will be{location: 'body'}
. IftransformerOptions
is{foo: 'bar'}
,options
will be{location: 'body', foo: 'bar'}
. IftransformerOptions
is{foo: 'bar', location: 'params'}
,options
will be{location: 'params', foo: 'bar'}
.
-
(optional)options: Object
: an optional options object with the following properties.-
(optional) validateOnly: boolean (default: false)
: whentrue
, ignore the value returned by the callback. In other words, this config specifies whether the transformation is a transformer (check and transform the value) or a validator (only check). -
(optional) force: boolean (default: false)
: unlesstrue
, when the input value is omitted, the transformation will be skipped.Note 1: the library uses
Object.hasOwnProperty()
to determine whether a value at a path exists, which means even if the input data is specified with anundefined
ornull
or any value, the transformation is not skipped regardless offorce
.Note 2: when
force
isfalse
, andpath
is an array of strings, the following rules are applied and overwriting the default behavior.- If all of the values specified by any element in
path
do not exist, skip the transformation (respecting the value offorce
). - Otherwise, if at least one the values specified by
path
exists,force
is forced to betrue
. And theinfo
param in the transformationcallback
will have the updatedforce
value.
Note 3: when
path
is an array of strings, if there is any of path's element which includes the array notation, and there is zero input data on that array, the transformation will be skipped. This is more obvious because zero times of any number is zero.How does the library fix the data shape? (regardless the value of
force
)At a point of a path traversal, when the access point is not a leaf node (for e.g.,
foo.bar
infoo.bar.baar
,bar
inbar[]
,foo[0].bar
infoo[].bar[]
). If the current value is omitted or is presented but in a malformed format, regardless offorce
, the value is reset to[]
or{}
according to the requirement. - If all of the values specified by any element in
-
-
Returned value: the chain itself
-
chain.message(callback, option)
: overwrite the error message of the one or more previous transformations in the chain.-
Parameters:
(required) callback: Function | string
: the string indicating the message, or the function which accepts the same parameters as ofchain.transform()
'scallback
(i.e.,value
andinfo
) and returns the string message or a promise which resolves the string message.
When being accessed, if the callback throws an error or returns a projected promise, the transformation chain will throw that error while processing.
(optional) option: Object
: an object specifying the behavior of the overwriting message, which includes the following properties.-
(optional) global: boolean (default: false)
:- if
global
istrue
: overwrite the error message of all transformations in the chain, which does not have an error message overwritten, from the beginning until when this message is called.
Note: the error message of the most recent transformation will be overwritten even if it exists, regardless of the value of
global
. If two consecutive messages are provided, the latter is preferred (with a configurationconsole.warn
's message). - if
-
-
Returned value: the chain itself
-
Initially, the library adds these chain plugins (by calling addTransformerPlugin
internally).
In these plugins' config, when the force
option exists, it indicates the force
config in the transformation.
These plugins only validate, do not change the inputs in the paths. In other words, they have a true
validateOnly
.
-
chain.exists({acceptEmptyString = false} = {})
: invalidate if the input isundefined
,null
,''
(empty string), or omitted. IfacceptEmptyString
istrue
, empty string is accepted as valid. -
chain.is(value, options?: {force?: boolean})
: check if the input is value. -
chain.isArray(options?: {force?: boolean})
: check if the input is an array. -
chain.isEmail(options)
: check if the input is a string and in email format.options
is an optional object with the following propertiesforce?: boolean
allowDisplayName?: boolean
requireDisplayName?: boolean
allowUtf8LocalPart?: boolean
requireTld?: boolean
ignoreMaxLength?: boolean
domainSpecificValidation?: boolean
allowIpDomain?: boolean
Please consult the validator package for more details.
-
chain.isIn(values, options?: {force?: boolean})
: check if the input is in the providedvalues
list. -
chain.isLength(options, transformOptions?: {force?: boolean})
: check the input's length. If the input is an array, check for the number of its elements. Otherwise if the input is a string, check for its length. Otherwise, throw an error.The
options
object can be a number (in number or in string format), or an object of type{min?: number, max?: number}
. Ifoptions
is a number, the transformation checks if the input's length has a fixed length. Otherwise, it validates the length bymin
,max
, if the option exists, accordingly. -
chain.isType(type, options?: {force?: boolean})
: check the result oftypeof
of the input value to betype
. -
chain.matches(regex, options?: {force?: boolean})
: check if the input is a string and matches a regex.
These plugins probably change the inputs in the paths. In other words, they have a false
validateOnly
.
-
chain.defaultValue(defaultValue, options?: {ignoreEmptyString?: boolean})
: change the input todefaultValue
ifvalue
isundefined
,null
,''
(empty string, unlessignoreEmptyString
istrue
), or omitted. -
chain.toDate(options?: {resetTime?: boolean, force?: boolean})
: convert the input to aDate
object. Throw an error if the input is not a number, not a string, not a BigInt, not a Date object. Otherwise, convert the input to Date object using the input value, throw an error if impossible.Parameter:
options
is an optional options object which can have the following properties. All are optional.force
resetTime?: boolean
: whentrue
, resethour
,minute
,second
, andmillisecond
of the converted Date object to zero.copy?: boolean
: whentrue
, and the input value is aDate
object, create a newDate
object.before?: Date | string | number | bigint
after?: Date | string | number | bigint
notBefore?: Date | string | number | bigint
notAfter?: Date | string | number | bigint
-
chain.toFloat(options?: {min?: number, max?: number, acceptInfinity?: boolean, force?: boolean})
: convert the input to a number. Throw an error if the input is an invalid number (using!isNaN()
andisFinite
(ifacceptInfinity
isfalse
)) or cannot be parsed to a number. Support range checking with themin
,max
in the options.Note:
bigint
value is converted tonumber
.NaN
is an invalid value. -
chain.toInt(options?: {min, max, force})
: convert the input to an integer number. Throw an error if the input is an invalid number (using!isNaN()
andisFinite
(ifacceptInfinity
isfalse
)) or cannot be parsed to a number. Support range checking with themin
,max
in the options.Note:
bigint
value is converted tonumber
.NaN
is an invalid value. -
chain.trim()
: trim value if it exists and is in string format. This transformer never throws any error. -
chain.use(pluginConfigs: Array<[ITransformPlugin | string, ...any[]]>)
: combine multiple plugins. Parameters:(required) pluginConfigs: array
: an array whose elements must have the following specification.- The first element is the plugin object or the name of an existing plugin (
'isType'
,'transform'
,'isLength'
, etc.). - The rest of the array contains the options which will be passed to the plugins.
Note:
use
is itself a plugin.Note: All default plugins are exported and available to be used in this way (or you can use their names, either).
const { transform, exists, isIn, isEmail, isLength, matches, message, toDate, toFloat, toInt, trim, defaultValue, is, isArray, isType, use } = require('express-transformer')
- The first element is the plugin object or the name of an existing plugin (
You can add your own plugin via calling addTransformerPlugin
. For example: isUUID
, isPostalCode
, isCreditCard
, toUpeerCase
, ...
Consult the validator package for more validators.
addTransformerPlugin
accepts only one object presenting the plugin configuration, which should include the following properties.
(rquired) name: string
: the name of the plugin, for example'isPostalCode'
.(optional, but generaly should be defined) getConfig: Function
: a function accepting any parameters which are the parameters provided to the plugin call (for e.g.,chain.isPostalCode(...params)
). . This function should return an object including the following properties.(required) transform: Function
: a function which accepts the same parameters as ofchain.transform
.(optional) options: Object
: the options object which will be passed to.transform()
. It is highly recommended to setvalidateOnly
option here to explicitly indicate that your plugin is a validator or a transformer.(optional) updateStack: stack => void
: for internal plugins (such as.message
) which modify the transformation stack. This method, if exists, is executed beforetransform
, if exists.
It is recommended to make use of the exported TransformationError
error when throwing an error.
Check plugins directory for sample code.
If you think a plugin is useful and should be included in the initial plugin list, please fire and PR.
Otherwise, you can publish your own plugin to a separate package and add it with addTransformerPlugin
.
When writing a plugin, please keep in mind that the input value can be anything.
It is extremely recommended that you should check the input value type via typeof
or instanceof
in the plugin, if you are going to publish it for general uses.
Side note: even if you overwrite methods (like .message()
and .transform()
), the core function is still protected and unaffected.
The transformation chain's interface can be exported via namespace and a global declaration from anywhere in your project like below.
declare global {
namespace ExpressTransformer {
export interface ITransformer<T, V, Options> {
isPostalCode(
value: T,
options?: {force?: boolean}
): ITransformer<T, string, Options>
}
}
}
const {TransformationError} = require('express-transformer')
-
If a transformation has an associated message, the error message is wrapped in a
TransformationError
object instance. Otherwise, the error thrown by the callback in.transform(callback)
is thrown. -
All default plugins throw only
TransformationError
error in the transformation unless when the configuration object is invalid. -
The error's detailed information can be accessed by
error.info
, which is theinfo
object passed to the.transform()
'scallback
. -
API:
constructor(message: string, info: ITransformCallbackInfo)
A versatile string which support accessing value at any deep level and array iteration.
Giving a context object obj
. The following path
values make the library to look at the appropriate location in the context object.
- For example, if
path
is'foo.bar'
, the library will look atobj.foo.bar
. - If
path
contains[]
, the library will iterate all value at the path right before the[]
's occurrences. - For example, if
path
isfoo[].bar.foo.baar[]
, the library will look atobj.foo[0].bar.foo.baar[0]
,obj.foo[1].bar.foo.baar[0]
,obj.foo[2].bar.foo.baar[0]
,obj.foo[0].bar.foo.baar[1]
.
This library is handly if you want to validate/transform every element in an array, or every pair of elements between many arrays.
To indicate an array iteration, use the []
notation in the path
value.
Let's see by examples:
-
If
path
is'foo[].bar.baar[]'
, the library will do the following- Travel to
req.body.foo
, if the current value is an array, iterator through all elements in the array. On each element, go deeper via the pathbar.baar
. - At
bar.baar
of the children object, iterate through all values in the array, pass it to the transformer callback. - If
validateOnly
isfalse
, replace the value by the result returned from the callback (Promise
returned value is also supported).
- Travel to
-
If
path
is an array. Things become more complicated.
Base on the common sense, we decided to manually force the validation (ignoring the value of force
when needed), to avoid several use cases.
Assume that you want to make an API to change user password. There are the following requirements which the API should satisfy.
- If
password
andpasswordConfirm
are omitted, skip changing the password and may change other provided values). - If
password
orpasswordConfirm
are provided, check if they equal with some condition (length, ...etc) before process the change.
transformer(['password', 'passwordConfirm']).transform(callback)
is designed to do that for you.
- The most useful path of the array of arrays iteration, is that if there are multiple elements in the
path
object (which is an array ofstring
) have the array notation ([]
), the library will pair them one by one, and pass their values in a list and call thecallback
. The library also replaces the returned values in the corresponding locations, ifvalidateOnly
isfalse
. Accordingly, whenvalidateOnly
isfalse
andpath
is an array, thecallback
is required to return an array.
Q1. How do I customize the error handler?
A1. Place your own error handler and check the error with instanceof
.
app.use(transformer().transform(), (err, req, res, next) => {
if (err instanceof TransformationError) {
console.log(err.info)
}
//...
})
See change logs