Skip to Content
Form with files

Form with files

Example of form with files using Zod validation. Notice that “portfolio samples” can be uploaded as multiple files as well as a single file, and the validation model for this field is defined as a union of z.file() and z.array(z.file()).

Result








Code

FormZodController.ts
ZodFormWithFilesExample.tsx
1import { z } from 'zod/v4';
2import { prefix, post, operation, type VovkOutput } from 'vovk';
3import { withZod } from 'vovk-zod';
4
5@prefix('form-zod')
6export default class FormZodController {
7 @operation({
8 summary: 'Submit form (Zod)',
9 description: 'Submit form with Zod validation',
10 })
11 @post('{id}')
12 static submitForm = withZod({
13 isForm: true,
14 body: z
15 .object({
16 email: z.email().meta({ description: 'User email' }),
17 resume: z
18 .file()
19 .mime('image/png')
20 .meta({ description: 'Resume file', examples: ['application/pdf'] }),
21 portfolioSamples: z.union([z.array(z.file()), z.file()]).meta({ description: 'Portfolio samples' }),
22 })
23 .meta({ description: 'User object' }),
24 params: z.object({
25 id: z.uuid().meta({ description: 'User ID' }),
26 }),
27 output: z
28 .object({
29 email: z.email().meta({ description: 'User email' }),
30 resume: z.object({
31 name: z.string().meta({ description: 'Resume file name', examples: ['resume.pdf'] }),
32 size: z.number().min(0).meta({ description: 'Resume file size' }),
33 type: z.string().meta({ description: 'Resume file type', examples: ['application/pdf'] }),
34 }),
35 portfolioSamples: z
36 .object({
37 name: z.string().meta({ description: 'Portfolio sample file name', examples: ['portfolio.zip'] }),
38 size: z.number().min(0).meta({ description: 'Portfolio sample file size' }),
39 type: z.string().meta({ description: 'Portfolio sample file type', examples: ['application/zip'] }),
40 })
41 .array()
42 .meta({ description: 'Array of portfolio sample files' }),
43 })
44 .meta({ description: 'Response object' }),
45 async handle(req, { id }) {
46 const { resume, portfolioSamples, email } = await req.vovk.form();
47
48 return {
49 email,
50 resume: {
51 name: resume.name,
52 size: resume.size,
53 type: resume.type,
54 },
55 portfolioSamples: (Array.isArray(portfolioSamples) ? portfolioSamples : [portfolioSamples]).map((file) => ({
56 name: file.name,
57 size: file.size,
58 type: file.type,
59 })),
60 } satisfies VovkOutput<typeof FormZodController.submitForm>;
61 },
62 });
63}
1import { z } from 'zod/v4';
2import { prefix, post, operation, type VovkOutput } from 'vovk';
3import { withZod } from 'vovk-zod';
4
5@prefix('form-zod')
6export default class FormZodController {
7 @operation({
8 summary: 'Submit form (Zod)',
9 description: 'Submit form with Zod validation',
10 })
11 @post('{id}')
12 static submitForm = withZod({
13 isForm: true,
14 body: z
15 .object({
16 email: z.email().meta({ description: 'User email' }),
17 resume: z
18 .file()
19 .mime('image/png')
20 .meta({ description: 'Resume file', examples: ['application/pdf'] }),
21 portfolioSamples: z.union([z.array(z.file()), z.file()]).meta({ description: 'Portfolio samples' }),
22 })
23 .meta({ description: 'User object' }),
24 params: z.object({
25 id: z.uuid().meta({ description: 'User ID' }),
26 }),
27 output: z
28 .object({
29 email: z.email().meta({ description: 'User email' }),
30 resume: z.object({
31 name: z.string().meta({ description: 'Resume file name', examples: ['resume.pdf'] }),
32 size: z.number().min(0).meta({ description: 'Resume file size' }),
33 type: z.string().meta({ description: 'Resume file type', examples: ['application/pdf'] }),
34 }),
35 portfolioSamples: z
36 .object({
37 name: z.string().meta({ description: 'Portfolio sample file name', examples: ['portfolio.zip'] }),
38 size: z.number().min(0).meta({ description: 'Portfolio sample file size' }),
39 type: z.string().meta({ description: 'Portfolio sample file type', examples: ['application/zip'] }),
40 })
41 .array()
42 .meta({ description: 'Array of portfolio sample files' }),
43 })
44 .meta({ description: 'Response object' }),
45 async handle(req, { id }) {
46 const { resume, portfolioSamples, email } = await req.vovk.form();
47
48 return {
49 email,
50 resume: {
51 name: resume.name,
52 size: resume.size,
53 type: resume.type,
54 },
55 portfolioSamples: (Array.isArray(portfolioSamples) ? portfolioSamples : [portfolioSamples]).map((file) => ({
56 name: file.name,
57 size: file.size,
58 type: file.type,
59 })),
60 } satisfies VovkOutput<typeof FormZodController.submitForm>;
61 },
62 });
63}
Last updated on