Data Transfer Object class built on TypeStacks's class-validator
. Allows you to build a class that validates input from outside sources and ensures its shape is correct. Full TypeScript support with strict, strong typing.
Table of contents
- Installation
- Release notes
- Usage
- Full API Documentation
- Comprehensive example
- Development and contributions
- License
npm i data-transfer-object
# or
yarn add data-transfer-object
Browser builds aren't available yet, but they are planned for the future.
See information about breaking changes and release notes here.
Create your own classes by importing and extending DataTransferObject
with your own properties, and using the appropriate validator decorators. This package re-exports all decorators and functions from TypeStack's class-validator
.
import { DataTransferObject, IsString } from 'data-transfer-object';
class MyDto extends DataTransferObject {
// Insert decorated properties here
@IsString()
myString: string;
}
TypeScript note: when using TypeScript with "strict": true
, you must use non-null assertions (!:
) when declaring class properties. Also, experimentalDecorators
and emitDecoratorMetadata
must be set to true
in your tsconfig.json
.
Once your class is defined, you can construct a new instance of it, pass your input into it, and run .validate()
on it. If your input is valid, it will return a plain object with the validated data. Otherwise, by default, a ValidationException
will be thrown.
try {
const validated = new MyDto(input).validate();
console.log(validated);
// validated === input
} catch (err) {
if (err instanceof ValidationException) {
console.error(err.validationErrors);
// e.g. if `input` was `{ myString: 1234 }`, this would output:
{ "myString": ["property myString should be a string"] }
}
}
You can use setValidationErrorHandler()
to register a callback to run whenever a .validate()
call fails on any object across your project. For instance:
import { setValidationErrorHandler, ValidationErrorMap } from 'data-transfer-object';
class CustomError extends Error {
statusCode = 400;
errors: ValidationErrorMap;
constructor(errors: ValidationErrorMap) {
super('Failed to validate');
this.errors = errors;
}
}
setValidationErrorHandler(errors => {
throw new CustomError(errors);
});
try {
new MyExampleDto({ invalidData: 42 }).validate();
} catch (err) {
if (err instanceof CustomError) {
console.error(`Request failed with error code ${err.statusCode}`);
console.error(err.errors);
}
}
Under the hood, data-transfer-object
formats the raw array of ValidationError
returned by class-validator
into an object mapping keys to properties and values to validation messages (aka a ValidationErrorMap
).
If you wish to get direct access to the array of ValidationError
in your error handler, you may pass in { rawErrors: true }
as a second parameter to .setValidationErrorHandler()
:
import { setValidationErrorHandler } from 'data-transfer-object';
setValidationErrorHandler(
errors => {
// `errors` here is now of type `ValidationError[]`.
console.error(errors);
// example output:
[
{
target: { myString: 12 },
value: 12,
property: 'myString',
children: [],
constraints: { isString: 'myString must be a string' },
},
];
},
{ rawErrors: true },
);
You may use .getValidationErrors()
to get an array of ValidationError
without having to thrown an actual error. To get the plain data later, use .toJSON()
.
const dto = new MyDto({ someData: 'data' });
const errors = dto.getValidationErrors();
if (errors.length) {
console.error(errors);
return;
}
const data = dto.toJSON();
Take a look at class-validator
's documentation to get information on all available validators and validation options.
.validate()
and .getValidationErrors()
will only run synchronous validators, which covers most use cases in a web application. If you would like to run asynchronous validators as well, use .validateAsync()
and .getValidationErrorsAsync()
respectively. These counterparts will always return a promise, even if there are no async validators on the validated object.
The most up-to-date documentation for all exported items from this package is automatically generated from code and available at https://danielegarciav.github.io/data-transfer-object/.
You may wish to uncheck the "Externals" checkbox option (top-right on desktop, cog icon on mobile) in order to hide documentation from class-validator
and focus only on what this package exports.
In the following example, we have an Express application that lets us sign up users.
- We define an
UserSignupInput
data transfer object class. - We define our own custom errors
ApiError
andApiValidationError
, which allow us to define a status code, among other things. - We define an Express error handler to catch any errors and format the result that will be sent back to our client.
- We wire everything up as an Express app, and make it so validation errors throw an
ApiValidationError
, which will be then appropriately handled by our Express error handler. - We can then write our signup controller.
// input-classes.ts
import { DataTransferObject, IsString, Length, MinLength } from 'data-transfer-object';
export class UserSignupInput extends DataTransferObject {
@IsString()
@Length(2, 36, {
message: 'username must be 2-36 characters long',
})
username!: string;
@IsString()
@MinLength(8, {
message: 'password must be at least 8 characters long',
})
password!: string;
}
// custom-errors.ts
import { ValidationErrorMap } from 'data-transfer-object';
export class ApiError extends Error {
type: string;
statusCode: number;
constructor(type: ApiErrorType, statusCode?: number, message?: string) {
super(message);
this.name = 'ApiError';
this.type = type;
this.message = message ?? 'Unspecified error';
this.statusCode = statusCode ?? 500;
}
}
export class ApiValidationError extends ApiError {
validationMessages: ValidationErrorMap;
constructor(errors: ValidationErrorMap) {
super('InvalidRequest', 400, 'Request is invalid');
this.validationMessages = errors;
}
}
// error-handler.ts
import { Request, Response, NextFunction } from 'express';
import { ApiError } from './custom-errors';
export function expressErrorHandler(
error: Error | ApiError,
req: Request,
res: Response,
next: NextFunction,
) {
const statusCode = error instanceof ApiError ? error.statusCode : 500;
const jsonResponse = { error: { ...error, statusCode } };
// Log non-ApiErrors to console
if (!(error instanceof ApiError)) console.error(error);
res.status(statusCode).json(jsonResponse);
}
// express-server.ts
import Express from 'express';
import { setValidationErrorHandler } from 'data-transfer-object';
import { expressErrorHandler } from './error-handler.ts';
import { ApiValidationError } from './custom-errors.ts';
import { signup } from './signup-controller.ts';
setValidationErrorHandler(errors => {
throw new ApiValidationError(errors);
});
export const app = Express();
app.use(Express.json());
app.use('/users/signup', signup);
app.use(expressErrorHandler);
// signup-controller.ts
import { Request, Response, NextFunction } from 'express';
import { User } from './models';
import { UserSignupInput } from './input-classes';
export async function signup(req: Request, res: Response, next: NextFunction) {
try {
const { username, password } = new UserSignupInput(req.body).validate();
await User.register(username, password);
return res.status(200).json({ success: true });
} catch (err) {
return next(err);
}
}
We may also use something like express-async-errors
in order to avoid having to wrap our async code in try/catch statements and manually calling next()
. This is something that will be automatically addressed by Express 5 once it is released.
Check package.json to find scripts related to installing dependencies, building, testing, linting and generating documentation. I am open to new issues and pull requests!
MIT