Skip to content

Commit

Permalink
Merge pull request #46 from austinwoon/chore/export-zod-serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
Evgeny Zakharov authored May 2, 2023
2 parents 99d7675 + 2038e9d commit 9d8434a
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 34 deletions.
119 changes: 85 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@

## Ecosystem

| Package | About |
| :-- | :-- |
| [nestjs-zod](https://github.com/risenforces/nestjs-zod) | A tools for integrating Zod into your NestJS application |
| [nestjs-zod-prisma](https://github.com/risenforces/nestjs-zod-prisma) | Generate Zod schemas from your Prisma schema |
| Package | About |
| :-------------------------------------------------------------------- | :------------------------------------------------------- |
| [nestjs-zod](https://github.com/risenforces/nestjs-zod) | A tools for integrating Zod into your NestJS application |
| [nestjs-zod-prisma](https://github.com/risenforces/nestjs-zod-prisma) | Generate Zod schemas from your Prisma schema |

## Core library features

Expand Down Expand Up @@ -54,10 +54,11 @@ yarn add nestjs-zod zod
```

Peer dependencies:

- `zod` - `>= 3.14.3`
- `@nestjs/common` - `>= 8.0.0` (required on server side)
- `@nestjs/core` - `>= 8.0.0` (required on server side)
- `@nestjs/swagger` - `>= 5.0.0` (only when using `patchNestJsSwagger`)
- `@nestjs/common` - `>= 8.0.0` (required on server side)
- `@nestjs/core` - `>= 8.0.0` (required on server side)
- `@nestjs/swagger` - `>= 5.0.0` (only when using `patchNestJsSwagger`)

All peer dependencies are marked as optional for better client side usage, but you need to install required ones when using `nestjs-zod` on server side.

Expand All @@ -73,6 +74,7 @@ All peer dependencies are marked as optional for better client side usage, but y
- [Using ZodGuard](#using-zodguard)
- [Creating custom guard](#creating-custom-guard)
- [Validation Exceptions](#validation-exceptions)
- [Using ZodSerializerInterceptor](#using-zodserializerinterceptor-for-output-validation)
- [Extended Zod](#extended-zod)
- [ZodDateString](#zoddatestring)
- [ZodPassword](#zodpassword)
Expand Down Expand Up @@ -103,7 +105,7 @@ const CredentialsSchema = z.object({
Zod's classes and types are re-exported too, but under `/z` scope for more clarity:

```ts
import { ZodString, ZodError, ZodIssue } from 'nestjs-zod/z'
import { ZodString, ZodError, ZodIssue } from 'nestjs-zod/z'
```

## Creating DTO from Zod schema
Expand All @@ -124,6 +126,7 @@ class CredentialsDto extends createZodDto(CredentialsSchema) {}
### Using DTO

DTO does two things:

- Provides a schema for `ZodValidationPipe`
- Provides a type from Zod schema for you

Expand Down Expand Up @@ -218,7 +221,8 @@ import { createZodValidationPipe } from 'nestjs-zod'

const MyZodValidationPipe = createZodValidationPipe({
// provide custom validation exception factory
createValidationException: (error: ZodError) => new BadRequestException('Ooops')
createValidationException: (error: ZodError) =>
new BadRequestException('Ooops'),
})
```

Expand All @@ -229,11 +233,13 @@ Sometimes, we need to validate user input before specific Guards. We can't use V
The solution is `ZodGuard`. It works just like `ZodValidationPipe`, except for that is doesn't transform the input.

It has 2 syntax forms:

- `@UseGuards(new ZodGuard('body', CredentialsSchema))`
- `@UseZodGuard('body', CredentialsSchema)`

Parameters:
1. The source - `'body' | 'query' | 'params'`

1. The source - `'body' | 'query' | 'params'`
2. Zod Schema or DTO (just like `ZodValidationPipe`)

When the data is invalid - it throws [ZodValidationException](#validation-exceptions).
Expand Down Expand Up @@ -261,7 +267,8 @@ import { createZodGuard } from 'nestjs-zod'

const MyZodGuard = createZodGuard({
// provide custom validation exception factory
createValidationException: (error: ZodError) => new BadRequestException('Ooops')
createValidationException: (error: ZodError) =>
new BadRequestException('Ooops'),
})
```

Expand All @@ -274,7 +281,11 @@ import { validate } from 'nestjs-zod'

validate(wrongThing, UserDto, (zodError) => new MyException(zodError)) // throws MyException

const validatedUser = validate(user, UserDto, (zodError) => new MyException(zodError)) // returns typed value when succeed
const validatedUser = validate(
user,
UserDto,
(zodError) => new MyException(zodError)
) // returns typed value when succeed
```

## Validation Exceptions
Expand All @@ -301,6 +312,7 @@ The default server response on validation error looks like that:
The reason of this structure is default `ZodValidationException`.

You can customize the exception by creating custom `nestjs-zod` entities using the factories:

- [Validation Pipe](#creating-custom-validation-pipe)
- [Guard](#creating-custom-guard)

Expand All @@ -321,6 +333,49 @@ export class ZodValidationExceptionFilter implements ExceptionFilter {
}
```

## Using ZodSerializerInterceptor for output validation

To ensure that a response conforms to a certain shape, you may use the `ZodSerializerInterceptor` interceptor.

This would be especially useful in prevent accidental data leaks.

This is similar to NestJs' `@ClassSerializerInterceptor` feature [here](https://docs.nestjs.com/techniques/serialization)

### Include `@ZodSerializerInterceptor` in application root

```ts
@Module({
...
providers: [
...,
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
],
})
export class AppModule {}
```

### Use `@ZodResponseDto` to define the shape of the response for endpoint in controller

```ts
const UserSchema = z.object({ username: string() })

export class UserDto extends createZodDto(UserSchema) {}
```

```ts
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@ZodResponseDto(UserDto)
getUser(id: number) {
return this.userService.findOne(id) // --> The native service method returns { username: string, password: string by default }
}
}
```

In the above example, despite the `userService.findOne` method returns `password`, the `password` property will be stripped out thanks to the `@ZodResponseDto` decorator.

## Extended Zod

As you learned in [Writing Zod Schemas](#writing-zod-schemas) section, `nestjs-zod` provides a special version of Zod. It helps you to validate the user input more accurately by using our custom schemas and methods.
Expand Down Expand Up @@ -365,31 +420,37 @@ z.dateString().weekend()
```

Valid `date` format examples:

- `2022-05-15`

Valid `date-time` format examples:

- `2022-05-02:08:33Z`
- `2022-05-02:08:33.000Z`
- `2022-05-02:08:33+00:00`
- `2022-05-02:08:33-00:00`
- `2022-05-02:08:33.000+00:00`

Errors:

- `invalid_date_string` - invalid date

- `invalid_date_string_format` - wrong format

Payload:

- `expected` - `'date' | 'date-time'`

- `invalid_date_string_direction` - not past/future

Payload:

- `expected` - `'past' | 'future'`

- `invalid_date_string_day` - not weekDay/weekend

Payload:

- `expected` - `'weekDay' | 'weekend'`

- `too_small` with `type === 'date_string_year'`
Expand Down Expand Up @@ -425,6 +486,7 @@ z.password().atLeastOne('special')
```

Errors:

- `invalid_password_no_digit`
- `invalid_password_no_lowercase`
- `invalid_password_no_uppercase`
Expand Down Expand Up @@ -458,26 +520,24 @@ Therefore, the error details is located inside `params` property:

```ts
const error = {
"code": "custom",
"message": "Invalid date, expected it to be the past",
"params": {
"isNestJsZod": true,
"code": "invalid_date_string_direction",
code: 'custom',
message: 'Invalid date, expected it to be the past',
params: {
isNestJsZod: true,
code: 'invalid_date_string_direction',

// payload is always located here in a flat view
"expected": "past"
expected: 'past',
},
"path": [
"date"
]
path: ['date'],
}
```

### Working with errors on the client side

Optionally, you can install `nestjs-zod` on the client side.

The library provides you a `/frontend` scope, that can be used to detect custom NestJS Zod issues and process them the way you want.
The library provides you a `/frontend` scope, that can be used to detect custom NestJS Zod issues and process them the way you want.

```ts
import { isNestJsZodIssue, NestJsZodIssue, ZodIssue } from 'nestjs-zod/frontend'
Expand All @@ -498,6 +558,7 @@ function mapToFormErrors(issues: ZodIssue[]) {
### Setup

Prerequisites:

- `@nestjs/swagger` with version `^5.0.0` installed

Apply a patch:
Expand Down Expand Up @@ -563,11 +624,7 @@ The output will be the following:
"sex": {
"description": "We respect your gender choice",
"type": "string",
"enum": [
"male",
"female",
"nonbinary"
]
"enum": ["male", "female", "nonbinary"]
},
"social": {
"type": "object",
Expand All @@ -581,13 +638,7 @@ The output will be the following:
"format": "date-time"
}
},
"required": [
"username",
"password",
"sex",
"social",
"birthDate"
]
"required": ["username", "password", "sex", "social", "birthDate"]
}
```

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { ZodValidationException } from './exception'
export { createZodGuard, UseZodGuard, ZodGuard } from './guard'
export { patchNestJsSwagger, zodToOpenAPI } from './openapi'
export { createZodValidationPipe, ZodValidationPipe } from './pipe'
export { ZodSerializerDto, ZodSerializerInterceptor } from './serializer'
export { validate } from './validate'

0 comments on commit 9d8434a

Please sign in to comment.