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
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}
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}
1'use client';
2import { useRef, useState, type FormEvent } from 'react';
3import { FormZodRPC } from 'vovk-client';
4import type { VovkReturnType } from 'vovk';
5
6export default function ZodFormExample() {
7 const [response, setResponse] = useState<VovkReturnType<typeof FormZodRPC.submitForm> | null>(null);
8 const [error, setError] = useState<Error | null>(null);
9 const ref = useRef<HTMLFormElement>(null);
10 const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
11 e.preventDefault();
12 try {
13 const formData = new FormData(ref.current!);
14 setResponse(
15 await FormZodRPC.submitForm({
16 body: formData,
17 params: { id: '5a279068-35d6-4d67-94e0-c21ef4052eea' },
18 })
19 );
20 setError(null);
21 } catch (e) {
22 setError(e as Error);
23 setResponse(null);
24 }
25 };
26
27 return (
28 <form onSubmit={onSubmit} ref={ref}>
29 <label htmlFor="email" className="font-bold">
30 Email
31 </label>
32 <input type="text" placeholder="Email" name="email" />
33 <br />
34 <br />
35 <label htmlFor="resume" className="font-bold">
36 Resume
37 </label>
38 <br />
39 <input type="file" placeholder="Resume" name="resume" />
40 <br />
41 <br />
42 <label htmlFor="portfolioSamples" className="font-bold">
43 Portfolio Samples (multiple)
44 </label>
45 <br />
46 <input type="file" multiple placeholder="Portfolio Samples" name="portfolioSamples" />
47 <br />
48 <button>Submit</button>
49
50 {response && (
51 <div className="text-left">
52 <h3>Response:</h3>
53 <pre>{JSON.stringify(response, null, 2)}</pre>
54 </div>
55 )}
56
57 {error && <div className="overflow-auto">❌ {String(error)}</div>}
58 </form>
59 );
60}
1'use client';
2import { useRef, useState, type FormEvent } from 'react';
3import { FormZodRPC } from 'vovk-client';
4import type { VovkReturnType } from 'vovk';
5
6export default function ZodFormExample() {
7 const [response, setResponse] = useState<VovkReturnType<typeof FormZodRPC.submitForm> | null>(null);
8 const [error, setError] = useState<Error | null>(null);
9 const ref = useRef<HTMLFormElement>(null);
10 const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
11 e.preventDefault();
12 try {
13 const formData = new FormData(ref.current!);
14 setResponse(
15 await FormZodRPC.submitForm({
16 body: formData,
17 params: { id: '5a279068-35d6-4d67-94e0-c21ef4052eea' },
18 })
19 );
20 setError(null);
21 } catch (e) {
22 setError(e as Error);
23 setResponse(null);
24 }
25 };
26
27 return (
28 <form onSubmit={onSubmit} ref={ref}>
29 <label htmlFor="email" className="font-bold">
30 Email
31 </label>
32 <input type="text" placeholder="Email" name="email" />
33 <br />
34 <br />
35 <label htmlFor="resume" className="font-bold">
36 Resume
37 </label>
38 <br />
39 <input type="file" placeholder="Resume" name="resume" />
40 <br />
41 <br />
42 <label htmlFor="portfolioSamples" className="font-bold">
43 Portfolio Samples (multiple)
44 </label>
45 <br />
46 <input type="file" multiple placeholder="Portfolio Samples" name="portfolioSamples" />
47 <br />
48 <button>Submit</button>
49
50 {response && (
51 <div className="text-left">
52 <h3>Response:</h3>
53 <pre>{JSON.stringify(response, null, 2)}</pre>
54 </div>
55 )}
56
57 {error && <div className="overflow-auto">❌ {String(error)}</div>}
58 </form>
59 );
60}
1'use client';
2import { useRef, useState, type FormEvent } from 'react';
3import { FormZodRPC } from 'vovk-client';
4import type { VovkReturnType } from 'vovk';
5
6export default function ZodFormExample() {
7 const [response, setResponse] = useState<VovkReturnType<typeof FormZodRPC.submitForm> | null>(null);
8 const [error, setError] = useState<Error | null>(null);
9 const ref = useRef<HTMLFormElement>(null);
10 const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
11 e.preventDefault();
12 try {
13 const formData = new FormData(ref.current!);
14 setResponse(
15 await FormZodRPC.submitForm({
16 body: formData,
17 params: { id: '5a279068-35d6-4d67-94e0-c21ef4052eea' },
18 })
19 );
20 setError(null);
21 } catch (e) {
22 setError(e as Error);
23 setResponse(null);
24 }
25 };
26
27 return (
28 <form onSubmit={onSubmit} ref={ref}>
29 <label htmlFor="email" className="font-bold">
30 Email
31 </label>
32 <input type="text" placeholder="Email" name="email" />
33 <br />
34 <br />
35 <label htmlFor="resume" className="font-bold">
36 Resume
37 </label>
38 <br />
39 <input type="file" placeholder="Resume" name="resume" />
40 <br />
41 <br />
42 <label htmlFor="portfolioSamples" className="font-bold">
43 Portfolio Samples (multiple)
44 </label>
45 <br />
46 <input type="file" multiple placeholder="Portfolio Samples" name="portfolioSamples" />
47 <br />
48 <button>Submit</button>
49
50 {response && (
51 <div className="text-left">
52 <h3>Response:</h3>
53 <pre>{JSON.stringify(response, null, 2)}</pre>
54 </div>
55 )}
56
57 {error && <div className="overflow-auto">❌ {String(error)}</div>}
58 </form>
59 );
60}
1'use client';
2import { useRef, useState, type FormEvent } from 'react';
3import { FormZodRPC } from 'vovk-client';
4import type { VovkReturnType } from 'vovk';
5
6export default function ZodFormExample() {
7 const [response, setResponse] = useState<VovkReturnType<typeof FormZodRPC.submitForm> | null>(null);
8 const [error, setError] = useState<Error | null>(null);
9 const ref = useRef<HTMLFormElement>(null);
10 const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
11 e.preventDefault();
12 try {
13 const formData = new FormData(ref.current!);
14 setResponse(
15 await FormZodRPC.submitForm({
16 body: formData,
17 params: { id: '5a279068-35d6-4d67-94e0-c21ef4052eea' },
18 })
19 );
20 setError(null);
21 } catch (e) {
22 setError(e as Error);
23 setResponse(null);
24 }
25 };
26
27 return (
28 <form onSubmit={onSubmit} ref={ref}>
29 <label htmlFor="email" className="font-bold">
30 Email
31 </label>
32 <input type="text" placeholder="Email" name="email" />
33 <br />
34 <br />
35 <label htmlFor="resume" className="font-bold">
36 Resume
37 </label>
38 <br />
39 <input type="file" placeholder="Resume" name="resume" />
40 <br />
41 <br />
42 <label htmlFor="portfolioSamples" className="font-bold">
43 Portfolio Samples (multiple)
44 </label>
45 <br />
46 <input type="file" multiple placeholder="Portfolio Samples" name="portfolioSamples" />
47 <br />
48 <button>Submit</button>
49
50 {response && (
51 <div className="text-left">
52 <h3>Response:</h3>
53 <pre>{JSON.stringify(response, null, 2)}</pre>
54 </div>
55 )}
56
57 {error && <div className="overflow-auto">❌ {String(error)}</div>}
58 </form>
59 );
60}
Last updated on