
Security News
CVE Volume Surges Past 48,000 in 2025 as WordPress Plugin Ecosystem Drives Growth
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.
express-transformer
Advanced tools
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.
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)
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)
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) => {}
)
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) => {}
)
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())}
)
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})
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})
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)
}
}
}
)
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)
}
}
}
)
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.
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: []}.
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()
}
)
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)
)
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()
)
declare global {
namespace ExpressTransformer {
export interface ITransformer<T, V, Options> {
isPostalCode(): ITransformer<T, string, 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})
)
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']
]),
)
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')
.use itself is a plugin.transformer('page', {location: 'param'}).use([
['defaultValue', 1]
['use', [['toInt', {min: 1}], ['transform', page => page - 1]]]
])
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)),
)
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()]
]))
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})
)
.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.
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 as default)addTransformerPluginTypically, 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
(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).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
path parameter in transformer(path, transformerOptions) is a string, value will be the value of the current input.path parameter in transformer(path, transformerOptions) is an array of string, value will be the array of the values of the list of current inputs.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 the path parameter is an array, this value will be an array of array.
req: the request req object from the connect-like middleware.
options: Object: the transformerOptions object passed in .transform(callback, transformerOptions) with default fields (i.e., the location field) filled.
Note: this options object is always a non-null object, with options.location reflects the currently being used value of the location config.
Which means, for e.g., if transformerOptions is undefined, options will be {location: 'body'}.
If transformerOptions is {foo: 'bar'}, options will be {location: 'body', foo: 'bar'}.
If transformerOptions 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): when true, 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): unless true, 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 an undefined or null or any value, the transformation is not skipped regardless of force.
Note 2: when force is false, and path is an array of strings, the following rules are applied and overwriting the default behavior.
path do not exist, skip the transformation (respecting the value of force).path exists, force is forced to be true.
And the info param in the transformation callback will have the updated force 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 in foo.bar.baar, bar in bar[], foo[0].bar in foo[].bar[]).
If the current value is omitted or is presented but in a malformed format, regardless of force, the value is reset to [] or {} according to the requirement.
Returned value: the chain itself
chain.message(callback, option): overwrite the error message of the one or more previous transformations in the chain.
(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.(optional) option: Object: an object specifying the behavior of the overwriting message, which includes the following properties.
(optional) global: boolean (default: false):
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.global.
If two consecutive messages are provided, the latter is preferred (with a configuration console.warn's message).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 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?: booleanallowDisplayName?: booleanrequireDisplayName?: booleanallowUtf8LocalPart?: booleanrequireTld?: booleanignoreMaxLength?: booleandomainSpecificValidation?: booleanallowIpDomain?: booleanPlease 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.
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.
forceresetTime?: 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 | bigintafter?: Date | string | number | bigintnotBefore?: Date | string | number | bigintnotAfter?: Date | string | number | bigintchain.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.
'isType', 'transform', 'isLength', etc.).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')
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.
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 the info object passed to the .transform()'s callback.
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.
path is 'foo.bar', the library will look at obj.foo.bar.path contains [], the library will iterate all value at the path right before the []'s occurrences.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].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:
path is 'foo[].bar.baar[]', the library will do the following
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.bar.baar of the children object, iterate through all values in the array, pass it to the transformer callback.validateOnly is false, replace the value by the result returned from the callback (Promise returned value is also supported).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.
password and passwordConfirm are omitted, skip changing the password and may change other provided values).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.
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.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
FAQs
An express transformer, validator library
We found that express-transformer demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.

Security News
Socket CEO Feross Aboukhadijeh joins Insecure Agents to discuss CVE remediation and why supply chain attacks require a different security approach.

Security News
Tailwind Labs laid off 75% of its engineering team after revenue dropped 80%, as LLMs redirect traffic away from documentation where developers discover paid products.