Skip to content

Commit c9f63de

Browse files
authored
feat(ajv-validator): Add Ajv validator middleware (#794)
1 parent b0320d9 commit c9f63de

File tree

10 files changed

+500
-0
lines changed

10 files changed

+500
-0
lines changed

.changeset/two-poets-vanish.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hono/ajv-validator': patch
3+
---
4+
5+
Add Ajv validator middleware
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: ci-ajv-validator
2+
on:
3+
push:
4+
branches: [main]
5+
paths:
6+
- 'packages/ajv-validator/**'
7+
pull_request:
8+
branches: ['*']
9+
paths:
10+
- 'packages/ajv-validator/**'
11+
12+
jobs:
13+
ci:
14+
runs-on: ubuntu-latest
15+
defaults:
16+
run:
17+
working-directory: ./packages/ajv-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
@@ -38,6 +38,7 @@
3838
"build:effect-validator": "yarn workspace @hono/effect-validator build",
3939
"build:conform-validator": "yarn workspace @hono/conform-validator build",
4040
"build:casbin": "yarn workspace @hono/casbin build",
41+
"build:ajv-validator": "yarn workspace @hono/ajv-validator build",
4142
"build": "run-p 'build:*'",
4243
"lint": "eslint 'packages/**/*.{ts,tsx}'",
4344
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",

packages/ajv-validator/README.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Ajv validator middleware for Hono
2+
3+
Validator middleware using [Ajv](https://github.com/ajv-validator/ajv) for [Hono](https://honojs.dev) applications.
4+
Define your schema with Ajv and validate incoming requests.
5+
6+
## Usage
7+
8+
No Hook:
9+
10+
```ts
11+
import { type JSONSchemaType } from 'ajv';
12+
import { ajvValidator } from '@hono/ajv-validator';
13+
14+
const schema: JSONSchemaType<{ name: string; age: number }> = {
15+
type: 'object',
16+
properties: {
17+
name: { type: 'string' },
18+
age: { type: 'number' },
19+
},
20+
required: ['name', 'age'],
21+
additionalProperties: false,
22+
} as const;
23+
24+
const route = app.post('/user', ajvValidator('json', schema), (c) => {
25+
const user = c.req.valid('json');
26+
return c.json({ success: true, message: `${user.name} is ${user.age}` });
27+
});
28+
```
29+
30+
Hook:
31+
32+
```ts
33+
import { type JSONSchemaType } from 'ajv';
34+
import { ajvValidator } from '@hono/ajv-validator';
35+
36+
const schema: JSONSchemaType<{ name: string; age: number }> = {
37+
type: 'object',
38+
properties: {
39+
name: { type: 'string' },
40+
age: { type: 'number' },
41+
},
42+
required: ['name', 'age'],
43+
additionalProperties: false,
44+
};
45+
46+
app.post(
47+
'/user',
48+
ajvValidator('json', schema, (result, c) => {
49+
if (!result.success) {
50+
return c.text('Invalid!', 400);
51+
}
52+
})
53+
//...
54+
);
55+
```
56+
57+
## Author
58+
59+
Illia Khvost <https://github.com/ikhvost>
60+
61+
## License
62+
63+
MIT

packages/ajv-validator/package.json

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "@hono/ajv-validator",
3+
"version": "0.0.0",
4+
"description": "Validator middleware using Ajv",
5+
"type": "module",
6+
"module": "dist/index.js",
7+
"types": "dist/index.d.ts",
8+
"files": [
9+
"dist"
10+
],
11+
"scripts": {
12+
"test": "vitest --run",
13+
"build": "tsup ./src/index.ts --format esm,cjs --dts",
14+
"publint": "publint",
15+
"release": "yarn build && yarn test && yarn publint && yarn publish"
16+
},
17+
"exports": {
18+
".": {
19+
"import": {
20+
"types": "./dist/index.d.ts",
21+
"default": "./dist/index.js"
22+
},
23+
"require": {
24+
"types": "./dist/index.d.cts",
25+
"default": "./dist/index.cjs"
26+
}
27+
}
28+
},
29+
"license": "MIT",
30+
"publishConfig": {
31+
"registry": "https://registry.npmjs.org",
32+
"access": "public"
33+
},
34+
"repository": {
35+
"type": "git",
36+
"url": "https://github.com/honojs/middleware.git"
37+
},
38+
"homepage": "https://github.com/honojs/middleware",
39+
"peerDependencies": {
40+
"ajv": ">=8.12.0",
41+
"hono": ">=3.9.0"
42+
},
43+
"devDependencies": {
44+
"ajv": ">=8.12.0",
45+
"hono": "^4.4.12",
46+
"tsup": "^8.1.0",
47+
"vitest": "^1.6.0"
48+
}
49+
}

packages/ajv-validator/src/index.ts

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type { Context, Env, MiddlewareHandler, ValidationTargets } from 'hono';
2+
import { validator } from 'hono/validator';
3+
import { Ajv, type JSONSchemaType, type ErrorObject } from 'ajv';
4+
5+
type Hook<T, E extends Env, P extends string> = (
6+
result: { success: true; data: T } | { success: false; errors: ErrorObject[] },
7+
c: Context<E, P>
8+
) => Response | Promise<Response> | void;
9+
10+
/**
11+
* Hono middleware that validates incoming data via an Ajv JSON schema.
12+
*
13+
* ---
14+
*
15+
* No Hook
16+
*
17+
* ```ts
18+
* import { type JSONSchemaType } from 'ajv';
19+
* import { ajvValidator } from '@hono/ajv-validator';
20+
*
21+
* const schema: JSONSchemaType<{ name: string; age: number }> = {
22+
* type: 'object',
23+
* properties: {
24+
* name: { type: 'string' },
25+
* age: { type: 'number' },
26+
* },
27+
* required: ['name', 'age'],
28+
* additionalProperties: false,
29+
* };
30+
*
31+
* const route = app.post('/user', ajvValidator('json', schema), (c) => {
32+
* const user = c.req.valid('json');
33+
* return c.json({ success: true, message: `${user.name} is ${user.age}` });
34+
* });
35+
* ```
36+
*
37+
* ---
38+
* Hook
39+
*
40+
* ```ts
41+
* import { type JSONSchemaType } from 'ajv';
42+
* import { ajvValidator } from '@hono/ajv-validator';
43+
*
44+
* const schema: JSONSchemaType<{ name: string; age: number }> = {
45+
* type: 'object',
46+
* properties: {
47+
* name: { type: 'string' },
48+
* age: { type: 'number' },
49+
* },
50+
* required: ['name', 'age'],
51+
* additionalProperties: false,
52+
* };
53+
*
54+
* app.post(
55+
* '/user',
56+
* ajvValidator('json', schema, (result, c) => {
57+
* if (!result.success) {
58+
* return c.text('Invalid!', 400);
59+
* }
60+
* })
61+
* //...
62+
* );
63+
* ```
64+
*/
65+
export function ajvValidator<
66+
T,
67+
Target extends keyof ValidationTargets,
68+
E extends Env = Env,
69+
P extends string = string
70+
>(
71+
target: Target,
72+
schema: JSONSchemaType<T>,
73+
hook?: Hook<T, E, P>
74+
): MiddlewareHandler<
75+
E,
76+
P,
77+
{
78+
in: { [K in Target]: T };
79+
out: { [K in Target]: T };
80+
}
81+
> {
82+
const ajv = new Ajv();
83+
const validate = ajv.compile(schema);
84+
85+
return validator(target, (data, c) => {
86+
const valid = validate(data);
87+
if (valid) {
88+
if (hook) {
89+
const hookResult = hook({ success: true, data: data as T }, c);
90+
if (hookResult instanceof Response || hookResult instanceof Promise) {
91+
return hookResult;
92+
}
93+
}
94+
return data as T;
95+
}
96+
97+
const errors = validate.errors || [];
98+
if (hook) {
99+
const hookResult = hook({ success: false, errors }, c);
100+
if (hookResult instanceof Response || hookResult instanceof Promise) {
101+
return hookResult;
102+
}
103+
}
104+
return c.json({ success: false, errors }, 400);
105+
});
106+
}

0 commit comments

Comments
 (0)