data-transfer-object
Version:
Data Transfer Object class built on `class-validator`.
296 lines (223 loc) • 10.3 kB
Markdown
# data-transfer-object
Data Transfer Object class built on [TypeStacks's `class-validator`](https://github.com/typestack/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.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of contents**
- [Installation](#installation)
- [For Node.js and bundlers (Webpack, Rollup, etc)](#for-nodejs-and-bundlers-webpack-rollup-etc)
- [For browsers (script tag)](#for-browsers-script-tag)
- [Release notes](#release-notes)
- [Usage](#usage)
- [Throwing a custom error on validation failure](#throwing-a-custom-error-on-validation-failure)
- [Accessing raw error data in custom error handler](#accessing-raw-error-data-in-custom-error-handler)
- [Get validation errors without throwing an error](#get-validation-errors-without-throwing-an-error)
- [Run async validators](#run-async-validators)
- [Full API Documentation](#full-api-documentation)
- [Comprehensive example](#comprehensive-example)
- [Development and contributions](#development-and-contributions)
- [License](#license)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Installation
### For Node.js and bundlers (Webpack, Rollup, etc)
```bash
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](changelog.md).
## 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`](https://github.com/typestack/class-validator).
```typescript
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.
```typescript
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:
```typescript
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()`:
```typescript
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()`.
```typescript
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:
```typescript
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](https://github.com/typestack/class-validator/) 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` and `ApiValidationError`, which allow us to define a status code, among other things.
- We define an [Express error handler](https://expressjs.com/en/guide/error-handling.html) 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.
```typescript
// 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;
}
```
```typescript
// 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;
}
}
```
```typescript
// 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);
}
```
```typescript
// 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);
```
```typescript
// 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`](https://github.com/davidbanham/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