Skip to content

Commit a5c20b3

Browse files
authored
feat: class validator middleware for Hono (#788)
* setup middleware package * add implementation middleware * add documentation + updae tsconfig for decorator handling * add tests * fix format class-validator middleware * Add Readme * Update changelog & changeset * update changelog 2 * update changelog 2 * fix working directory ci * rm jest dependencies change to tsup for build fix ci name * revert changes not related to class-validator * remove the changeset since Changesets will add a changeset automatically * package description
1 parent 28ca0f3 commit a5c20b3

File tree

11 files changed

+1212
-1
lines changed

11 files changed

+1212
-1
lines changed

.changeset/spotty-donuts-yawn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hono/class-validator': major
3+
---
4+
5+
First release
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: ci-class-validator
2+
on:
3+
push:
4+
branches: [main]
5+
paths:
6+
- 'packages/class-validator/**'
7+
pull_request:
8+
branches: ['*']
9+
paths:
10+
- 'packages/class-validator/**'
11+
12+
jobs:
13+
ci:
14+
runs-on: ubuntu-latest
15+
defaults:
16+
run:
17+
working-directory: ./packages/class-validator
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: 20.x
23+
- run: yarn install --frozen-lockfile
24+
- run: yarn build
25+
- run: yarn test

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"scripts": {
1111
"build:hello": "yarn workspace @hono/hello build",
1212
"build:zod-validator": "yarn workspace @hono/zod-validator build",
13+
"build:class-validator": "yarn workspace @hono/class-validator build",
1314
"build:arktype-validator": "yarn workspace @hono/arktype-validator build",
1415
"build:qwik-city": "yarn workspace @hono/qwik-city build",
1516
"build:graphql-server": "yarn workspace @hono/graphql-server build",

packages/class-validator/README.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Class-validator middleware for Hono
2+
3+
The validator middleware using [class-validator](https://github.com/typestack/class-validator) for [Hono](https://github.com/honojs/hono) applications.
4+
5+
## Usage
6+
7+
```ts
8+
import { classValidator } from '@hono/class-validator'
9+
import { IsInt, IsString } from 'class-validator'
10+
11+
class CreateUserDto {
12+
@IsString()
13+
name!: string;
14+
15+
@IsInt()
16+
age!: number;
17+
}
18+
19+
20+
const route = app.post('/user', classValidator('json', CreateUserDto), (c) => {
21+
const user = c.req.valid('json')
22+
return c.json({ success: true, message: `${user.name} is ${user.age}` })
23+
})
24+
```
25+
26+
With hook:
27+
28+
```ts
29+
import { classValidator } from '@hono/class-validator'
30+
import { IsInt, IsString } from 'class-validator'
31+
32+
class CreateUserDto {
33+
@IsString()
34+
name!: string;
35+
36+
@IsInt()
37+
age!: number;
38+
}
39+
40+
app.post(
41+
'/user', classValidator('json', CreateUserDto, (result, c) => {
42+
if (!result.success) {
43+
return c.text('Invalid!', 400)
44+
}
45+
})
46+
//...
47+
)
48+
```
49+
50+
## Author
51+
52+
**Pr0m3ht3us** - https://github.com/pr0m3th3usex
53+
54+
## License
55+
56+
MIT

packages/class-validator/package.json

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "@hono/class-validator",
3+
"packageManager": "[email protected]",
4+
"description": "Validator middleware using class-validator",
5+
"version": "0.1.0",
6+
"type": "module",
7+
"main": "dist/index.js",
8+
"module": "dist/index.js",
9+
"types": "dist/index.d.ts",
10+
"exports": {
11+
".": {
12+
"import": "./dist/index.js",
13+
"types": "./dist/index.d.ts"
14+
}
15+
},
16+
"scripts": {
17+
"test": "vitest --run",
18+
"build": "rimraf dist && tsup ./src/index.ts --format esm,cjs --dts",
19+
"prerelease": "yarn build && yarn test",
20+
"release": "yarn publish"
21+
},
22+
"license": "MIT",
23+
"publishConfig": {
24+
"registry": "https://registry.npmjs.org",
25+
"access": "public"
26+
},
27+
"repository": {
28+
"type": "git",
29+
"url": "https://github.com/honojs/middleware.git"
30+
},
31+
"homepage": "https://github.com/honojs/middleware",
32+
"peerDependencies": {
33+
"hono": ">=3.9.0"
34+
},
35+
"devDependencies": {
36+
"hono": "^4.0.10",
37+
"rimraf": "^5.0.5",
38+
"tsup": "^8.3.5",
39+
"typescript": "^5.3.3",
40+
"vitest": "^1.4.0"
41+
},
42+
"dependencies": {
43+
"class-transformer": "^0.5.1",
44+
"class-validator": "^0.14.1",
45+
"reflect-metadata": "^0.2.2"
46+
}
47+
}

packages/class-validator/src/index.ts

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import 'reflect-metadata'
2+
import { validator } from 'hono/validator'
3+
import { ClassConstructor, ClassTransformOptions, plainToClass } from 'class-transformer'
4+
import { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'
5+
import { ValidationError, validate } from 'class-validator'
6+
7+
/**
8+
* Hono middleware that validates incoming data using class-validator(https://github.com/typestack/class-validator).
9+
*
10+
* ---
11+
*
12+
* No Hook
13+
*
14+
* ```ts
15+
* import { classValidator } from '@hono/class-validator'
16+
* import { IsInt, IsString } from 'class-validator'
17+
*
18+
* class CreateUserDto {
19+
* @IsString()
20+
* name!: string;
21+
*
22+
* @IsInt()
23+
* age!: number;
24+
* }
25+
*
26+
*
27+
* const route = app.post('/user', classValidator('json', CreateUserDto), (c) => {
28+
* const user = c.req.valid('json')
29+
* return c.json({ success: true, message: `${user.name} is ${user.age}` })
30+
* })
31+
* ```
32+
*
33+
* ---
34+
* Hook
35+
*
36+
* ```ts
37+
* import { classValidator } from '@hono/class-validator'
38+
* import { IsInt, IsString } from 'class-validator'
39+
*
40+
* class CreateUserDto {
41+
* @IsString()
42+
* name!: string;
43+
*
44+
* @IsInt()
45+
* age!: number;
46+
* }
47+
*
48+
* app.post(
49+
* '/user',
50+
* classValidator('json', CreateUserDto, (result, c) => {
51+
* if (!result.success) {
52+
* return c.text('Invalid!', 400)
53+
* }
54+
* })
55+
* //...
56+
* )
57+
* ```
58+
*/
59+
60+
type Hook<
61+
T,
62+
E extends Env,
63+
P extends string,
64+
Target extends keyof ValidationTargets = keyof ValidationTargets,
65+
O = object
66+
> = (
67+
result: ({ success: true } | { success: false; errors: ValidationError[] }) & {
68+
data: T
69+
target: Target
70+
},
71+
c: Context<E, P>
72+
) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>
73+
74+
type HasUndefined<T> = undefined extends T ? true : false
75+
76+
type HasClassConstructor<T> = ClassConstructor<any> extends T ? true : false
77+
78+
export type StaticObject<T extends ClassConstructor<any>> = {
79+
[K in keyof InstanceType<T>]: HasClassConstructor<InstanceType<T>[K]> extends true
80+
? StaticObject<InstanceType<T>[K]>
81+
: InstanceType<T>[K]
82+
}
83+
84+
const parseAndValidate = async <T extends ClassConstructor<any>>(
85+
dto: T,
86+
obj: object,
87+
options: ClassTransformOptions
88+
): Promise<
89+
{ success: false; errors: ValidationError[] } | { success: true; output: InstanceType<T> }
90+
> => {
91+
// tranform the literal object to class object
92+
const objInstance = plainToClass(dto, obj, options)
93+
// validating and check the errors, throw the errors if exist
94+
95+
const errors = await validate(objInstance)
96+
// errors is an array of validation errors
97+
if (errors.length > 0) {
98+
return {
99+
success: false,
100+
errors,
101+
}
102+
}
103+
104+
return { success: true, output: objInstance as InstanceType<T> }
105+
}
106+
107+
export const classValidator = <
108+
T extends ClassConstructor<any>,
109+
Output extends InstanceType<T> = InstanceType<T>,
110+
Target extends keyof ValidationTargets = keyof ValidationTargets,
111+
E extends Env = Env,
112+
P extends string = string,
113+
In = StaticObject<T>,
114+
I extends Input = {
115+
in: HasUndefined<In> extends true
116+
? {
117+
[K in Target]?: K extends 'json'
118+
? In
119+
: HasUndefined<keyof ValidationTargets[K]> extends true
120+
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
121+
: { [K2 in keyof In]: ValidationTargets[K][K2] }
122+
}
123+
: {
124+
[K in Target]: K extends 'json'
125+
? In
126+
: HasUndefined<keyof ValidationTargets[K]> extends true
127+
? { [K2 in keyof In]?: ValidationTargets[K][K2] }
128+
: { [K2 in keyof In]: ValidationTargets[K][K2] }
129+
}
130+
out: { [K in Target]: Output }
131+
},
132+
V extends I = I
133+
>(
134+
target: Target,
135+
dataType: T,
136+
hook?: Hook<Output, E, P, Target>,
137+
options: ClassTransformOptions = { enableImplicitConversion: false }
138+
): MiddlewareHandler<E, P, V> =>
139+
// @ts-expect-error not typed well
140+
validator(target, async (data, c) => {
141+
const result = await parseAndValidate(dataType, data, options)
142+
143+
if (hook) {
144+
const hookResult = hook({ ...result, data, target }, c)
145+
if (hookResult instanceof Response || hookResult instanceof Promise) {
146+
if ('response' in hookResult) {
147+
return hookResult.response
148+
}
149+
return hookResult
150+
}
151+
}
152+
153+
if (!result.success) {
154+
return c.json({ errors: result.errors }, 400)
155+
}
156+
157+
return result.output
158+
})

0 commit comments

Comments
 (0)