data-transfer-object
v0.4.0
Published
Data Transfer Object class built on `class-validator`.
Downloads
608
Maintainers
Readme
data-transfer-object
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
Installation
For Node.js and bundlers (Webpack, Rollup, etc)
npm i data-transfer-object
# or
yarn add data-transfer-object
For browsers (script tag)
Browser builds aren't available yet, but they are planned for the future.
Release notes
See information about breaking changes and release notes here.
Usage
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"] }
}
}
Throwing a custom error on validation failure
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);
}
}
Accessing raw error data in custom error handler
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 },
);
Get validation errors without throwing an error
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();
Validating it will also silently drop all unknown properties from the input:
const input = new MyDto({ myString: 'a', myOtherString: 'b' });
const errors = input.validate();
console.log(errors); // []
console.log(input); // { myString: 'a' }
Take a look at class-validator
's documentation to get information on all available validators and validation options.
Run async validators
.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.
Full API Documentation
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.
Comprehensive example
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.
Development and contributions
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!
License
MIT