Express transformer 

Connect-like middleware to validate/transform data.
Table of contents
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.
Usage samples
const express = require('express')
const {transformer} = require('express-transformer')
const app = express()
app.post('/login',
transformer('username').exists(),
transformer('password').exists(),
(req, res, next) => {
})
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) => {
})
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 => {
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
})
.message(async id => {
return `${id} is not a valid user id`
}),
(req, res) => {res.status(200).json(req.params.id.toJSON())}
)
app.get('/admin/update',
transformer('token')
.exists()
.is('secret-value')
.message('Invalid credential', {global: true}),
(req, res) => {}
)
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]) => {
}, {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) => {
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}),
(req, res) => {
for (const {config: {categories}} of req.body.products || []) {
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 (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'],
['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}) => {
(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,
{
req,
path,
pathSplits,
options: {
location,
rawPath,
rawLocation,
disableArrayNotation
}
}
) => {
(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.
General usage
The library exports the following methods.
transformer (also exported as default)
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) => {})
API references
Create a transformation chain
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 the req object.
(optional) rawLocation: boolean (default: false): treat the location 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.
Transformation chain
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.
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 of chain.transform()'s callback (i.e., value and info)
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 is true: 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 configuration console.warn's message).
- Returned value: the chain itself
Plugins
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.
Validators:
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 is undefined, null, '' (empty string), or omitted.
If acceptEmptyString is true, 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 properties
force?: 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 provided values 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}.
If options is a number, the transformation checks if the input's length has a fixed length.
Otherwise, it validates the length by min, max, if the option exists, accordingly.
-
chain.isType(type, options?: {force?: boolean}): check the result of typeof of the input value to be type.
-
chain.matches(regex, options?: {force?: boolean}): check if the input is a string and matches a regex.
Transformers:
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 to defaultValue if value is undefined, null, '' (empty string, unless ignoreEmptyString is true), or omitted.
-
chain.toDate(options?: {resetTime?: boolean, force?: boolean}): convert the input to a Date 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: when true, reset hour, minute, second, and millisecond of the converted Date object to zero.
copy?: boolean: when true, and the input value is a Date object, create a new Date 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() and isFinite (if acceptInfinity is false)) or cannot be parsed to a number.
Support range checking with the min, max in the options.
Note: bigint value is converted to number. 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() and isFinite (if acceptInfinity is false)) or cannot be parsed to a number.
Support range checking with the min, max in the options.
Note: bigint value is converted to number. 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')
How to add a custom 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 of chain.transform.
(optional) options: Object: the options object which will be passed to .transform().
It is highly recommended to set validateOnly 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 before transform, 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.
How to extend the Typescript typing.
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>
}
}
}
Error class
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 the info object passed to the .transform()'s callback.
-
API: constructor(message: string, info: ITransformCallbackInfo)
Annotations explanation
Universal path format
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 at obj.foo.bar.
- If
path contains [], the library will iterate all value at the path right before the []'s occurrences.
- For example, if
path is foo[].bar.foo.baar[], the library will look at obj.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].
Array of arrays iteration
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 path bar.baar.
- At
bar.baar of the children object, iterate through all values in the array, pass it to the transformer callback.
- If
validateOnly is false, replace the value by the result returned from the callback (Promise returned value is also supported).
- 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 and passwordConfirm are omitted, skip changing the password and may change other provided values).
- If
password or passwordConfirm 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 of string) have the array notation ([]),
the library will pair them one by one, and pass their values in a list and call the callback.
The library also replaces the returned values in the corresponding locations, if validateOnly is false.
Accordingly, when validateOnly is false and path is an array, the callback is required to return an array.
QA
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)
}
//...
})
Change logs
See change logs