Isomorphic Zod validation for Vovk.ts controllers on server and client
vovk-zod exports vovkZod
decorator fabric that validates request body and incoming query string with Zod models.
// /src/models/user/UserController.ts
import { z } from 'zod';
import vovkZod from 'vovk-zod';
import { put, type VovkRequest } from 'vovk';
import UserService from './UserService';
const UpdateUserModel = z.object({
name: z.string(),
email: z.string(),
}).strict();
const UpdateUserQueryModel = z.object({
id: z.string(),
}).strict();
export default class UserController {
private static userService = UserService;
@put()
@vovkZod(UpdateUserModel, UpdateUserQueryModel)
static updateUser(
req: VovkRequest<z.infer<typeof UpdateUserModel>, z.infer<typeof UpdateUserQueryModel>>
) {
const { name, email } = await req.json();
const id = req.nextUrl.searchParams.get('id');
return this.userService.updateUser(id, { name, email });
}
}
'use client';
import React from 'react';
import { UserController } from 'vovk-client';
const MyPage = () => {
useEffect(() => {
void UserController.updateUser({
query: { id: '696969' },
body: { name: 'John Doe', email: 'john@example.com' },
// optionally, disable client validation for debugging purpose
disableClientValidation: true,
}).then(/* ... */);
}, []);
return (
// ...
)
}
export default MyPage;
When vovk-zod is installed zodValidateOnClient is enabled by default as validateOnClient
config option to validate incoming reqests on the client-side. Please check customization docs for more info.
Hint: To produce less variables you can also declare Zod models as static
(with an optional private
prefix) class members of the controller to access them within the @vovkZod
decorator and VovkRequest
.
// ...
export default class UserController {
private static userService = UserService;
private static UpdateUserModel = z.object({
name: z.string(),
email: z.string(),
}).strict();
private static UpdateUserQueryModel = z.object({
id: z.string(),
}).strict();
@put()
@vovkZod(UserController.UpdateUserModel, UserController.UpdateUserQueryModel)
static updateUser(
req: VovkRequest<
z.infer<typeof UserController.UpdateUserModel>,
z.infer<typeof UserController.UpdateUserQueryModel>
>
) {
// ...
}
}
The TypeScript compiler processes decorators at compile time and doesn't enforce private or protected access restrictions for members used within decorators in the same class. This behavior allows for more flexible class meta-programming patterns, which decorators aim to facilitate.
The library doesn't support FormData
validation, but you can still validate query by setting body validation to null
.
// ...
export default class HelloController {
@post()
@vovkZod(null, z.object({ something: z.string() }).strict())
static postFormData(req: VovkRequest<FormData, { something: string }>) {
const formData = await req.formData();
const something = req.nextUrl.searchParams.get('something');
// ...
}
}
The library (as well as Vovk.ts itself) is built thanks to fantastic job made by other people.
- When
@vovkZod
is initialised, it converts Zod schemas to JSON Schemas with zod-to-json-schema and makes metadata handler to receive it as client validation object. -
@vovkZod
performs Zod validation on server-side. - When clientized controller method gets called
zodValidateOnClient
performs validation on client-side with Ajv.