Skip to main content
RapidDev - Software Development Agency

How to Build Form builder backend with V0

Build a form builder backend with V0 using Next.js, Supabase for form definitions and submissions, and dynamic Zod validation. You'll create a drag-and-drop form creator, public form renderer, submission analytics, and a dynamic validation engine — all in about 1-2 hours without touching a terminal.

What you'll build

  • Visual form builder with draggable field types and live preview using dnd-kit and Tabs
  • Dynamic form renderer that generates shadcn/ui components from stored JSON field definitions
  • Server-side Zod schema generation from form field configurations for submission validation
  • Submission analytics with field-level response distribution using Recharts BarChart
  • Public form pages with SEO metadata via generateMetadata
  • Support for 12 field types: text, textarea, email, number, select, multi-select, checkbox, radio, date, file, rating, scale
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate10 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build a form builder backend with V0 using Next.js, Supabase for form definitions and submissions, and dynamic Zod validation. You'll create a drag-and-drop form creator, public form renderer, submission analytics, and a dynamic validation engine — all in about 1-2 hours without touching a terminal.

What you're building

Form builders let non-technical users create custom forms for surveys, registrations, and data collection. Instead of coding each form from scratch, users design forms visually and share them via public URLs.

V0 generates the builder interface, form renderer, and analytics dashboard from prompts. Use prompt queuing to build each component separately. The key technical challenge is dynamic validation: form field definitions stored as JSON are converted to Zod schemas at submission time.

The architecture uses Next.js App Router with a client component for the drag-and-drop builder, a hybrid Server/Client Component for the public form page, an API route for submission validation, and Supabase for storing form definitions and responses.

Final result

A Typeform-style form builder with visual editor, 12 field types, dynamic server-side validation, public sharing, and submission analytics.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
dnd-kitDrag and Drop

Prerequisites

  • A V0 account (Premium plan for builder and renderer iterations)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • Understanding of form concepts (fields, validation, submissions)
  • A use case: surveys, registrations, feedback, or data collection

Build steps

1

Set up the project and form database schema

Open V0 and create a new project. Connect Supabase. Create the schema for forms (with JSON field definitions), submissions, and analytics tracking.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a form builder. Create a Supabase schema with:
3// 1. forms: id (uuid PK), owner_id (uuid FK to auth.users), title (text), description (text), fields (jsonb), settings (jsonb default '{"is_published": false, "requires_auth": false, "redirect_url": null, "submit_message": "Thank you!"}'), slug (text unique), submission_count (int default 0), created_at (timestamptz), updated_at (timestamptz)
4// The fields jsonb stores: [{id, type, label, placeholder, required, options[], validation}]
5// type is one of: text, textarea, email, number, select, multi_select, checkbox, radio, date, file, rating, scale
6// 2. submissions: id (uuid PK), form_id (uuid FK to forms), data (jsonb), submitter_id (uuid FK to auth.users nullable), ip_address (text), created_at (timestamptz)
7// 3. form_analytics: id (uuid PK), form_id (uuid FK to forms), date (date), views (int default 0), starts (int default 0), completions (int default 0), unique(form_id, date))
8// Add RLS: owners manage their forms, anyone can submit to published forms.

Pro tip: Queue the schema prompt first, then the builder UI, then the renderer — up to 10 prompts can be queued while V0 generates.

Expected result: Database schema created with JSON field storage and analytics tracking.

2

Build the visual form builder with drag-and-drop

Create the form builder page where users drag field types from a palette, reorder them, and configure each field's properties. A live preview shows how the form looks to respondents.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a form builder page at app/builder/[id]/page.tsx as a 'use client' component.
3// Requirements:
4// - Left sidebar: field type palette with draggable Card items for each of the 12 types
5// - Center: sortable field list using dnd-kit. Each field shows: label, type Badge, required indicator
6// - Right sidebar: field settings editor when a field is selected:
7// - Input for label, Input for placeholder
8// - Switch for required toggle
9// - For select/radio/multi_select: Input list for options (add/remove)
10// - For number/scale: min and max Input
11// - For text/textarea: max length Input
12// - Top bar: form title Input, "Preview" and "Settings" Tabs, Publish Switch
13// - Tabs to switch between Builder (editor) and Preview (rendered form)
14// - Settings tab: submit message Textarea, redirect URL Input, requires auth Switch
15// - Auto-save: debounced Server Action that saves field definitions to the forms.fields jsonb
16// - Use Card for the field palette items, Dialog for field settings on mobile

Expected result: Users can drag field types, reorder them, configure properties, and see a live preview. Changes auto-save.

3

Create the dynamic form renderer

Build the public form page that reads the JSON field definition and renders the appropriate shadcn/ui components. The form is a Server Component that fetches the definition, with a client component for interactive submission.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build the public form renderer at app/forms/[slug]/page.tsx.
3// Requirements:
4// - Server Component that fetches the form by slug
5// - Use generateMetadata to set page title from form.title for SEO
6// - If form is not published, show "This form is not available" message
7// - Render a 'use client' FormRenderer component that maps each field in the fields jsonb to the corresponding shadcn/ui component:
8// - text → Input
9// - textarea → Textarea
10// - email → Input type="email"
11// - number → Input type="number"
12// - select → Select with SelectItem for each option
13// - multi_select → Checkbox group for each option
14// - checkbox → Checkbox with label
15// - radio → RadioGroup with RadioGroupItem for each option
16// - date → Calendar/DatePicker
17// - file → Input type="file"
18// - rating → custom 5-star rating using Button components
19// - scale → Slider or numbered Button row
20// - Required fields show a red asterisk next to the label
21// - Submit Button POSTs to /api/submit/[slug] and shows the success message from settings
22// - Use Card to wrap the form with title and description at top

Expected result: Public form pages render the correct UI component for each field type and submit responses to the API.

4

Build the submission API with dynamic Zod validation

Create the API route that validates submission data against the form's field definitions by dynamically building a Zod schema. This ensures server-side validation matches the form configuration.

app/api/submit/[slug]/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3import { z } from 'zod'
4
5const supabase = createClient(
6 process.env.SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9
10type FieldDef = {
11 id: string; type: string; label: string;
12 required?: boolean; options?: string[];
13 validation?: { min?: number; max?: number; maxLength?: number };
14}
15
16function buildZodSchema(fields: FieldDef[]) {
17 const shape: Record<string, z.ZodTypeAny> = {}
18
19 for (const field of fields) {
20 let validator: z.ZodTypeAny
21
22 switch (field.type) {
23 case 'email': validator = z.string().email(); break
24 case 'number': case 'scale': case 'rating':
25 validator = z.number()
26 if (field.validation?.min) validator = (validator as z.ZodNumber).min(field.validation.min)
27 if (field.validation?.max) validator = (validator as z.ZodNumber).max(field.validation.max)
28 break
29 case 'checkbox': validator = z.boolean(); break
30 case 'multi_select': validator = z.array(z.string()); break
31 case 'date': validator = z.string(); break
32 default: validator = z.string()
33 if (field.validation?.maxLength) validator = (validator as z.ZodString).max(field.validation.maxLength)
34 }
35
36 shape[field.id] = field.required ? validator : validator.optional()
37 }
38
39 return z.object(shape)
40}
41
42export async function POST(
43 req: NextRequest,
44 { params }: { params: Promise<{ slug: string }> }
45) {
46 const { slug } = await params
47 const body = await req.json()
48
49 const { data: form } = await supabase
50 .from('forms')
51 .select('id, fields, settings')
52 .eq('slug', slug)
53 .single()
54
55 if (!form || !form.settings.is_published) {
56 return NextResponse.json({ error: 'Form not found' }, { status: 404 })
57 }
58
59 const schema = buildZodSchema(form.fields)
60 const result = schema.safeParse(body)
61
62 if (!result.success) {
63 return NextResponse.json({ error: result.error.flatten() }, { status: 400 })
64 }
65
66 await supabase.from('submissions').insert({
67 form_id: form.id,
68 data: result.data,
69 ip_address: req.headers.get('x-forwarded-for') ?? 'unknown',
70 })
71
72 await supabase.rpc('increment_submission_count', { form_id: form.id })
73
74 return NextResponse.json({ message: form.settings.submit_message })
75}

Expected result: The API dynamically builds a Zod schema from the form's field definitions and validates every submission server-side.

5

Build the submission results and analytics page

Create the results page showing all submissions and field-level response analytics with charts.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a results page at app/forms/[id]/results/page.tsx.
3// Requirements:
4// - Server Component fetching form with all submissions
5// - Summary Cards: total submissions, submissions today, completion rate (if analytics tracked)
6// - Table showing all submissions with columns dynamically generated from field labels
7// - Each cell shows the response value formatted by type (dates formatted, arrays joined, etc.)
8// - Click a row to see full submission detail in a Sheet
9// - Analytics section: for select/radio/checkbox fields, show a Recharts BarChart of response distribution
10// - For rating/scale fields, show average value and distribution histogram
11// - For text fields, show a word cloud or most common responses
12// - Tabs to switch between Table view and Analytics view
13// - Export Button to download all submissions as CSV
14// - Use Badge for field type indicators in table headers

Expected result: The results page shows submissions in a dynamic table and field-level analytics with charts for select, rating, and scale fields.

Complete code

app/api/submit/[slug]/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3import { z } from 'zod'
4
5const supabase = createClient(
6 process.env.SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9
10type FieldDef = {
11 id: string
12 type: string
13 label: string
14 required?: boolean
15 validation?: { min?: number; max?: number; maxLength?: number }
16}
17
18function buildZodSchema(fields: FieldDef[]) {
19 const shape: Record<string, z.ZodTypeAny> = {}
20 for (const field of fields) {
21 let v: z.ZodTypeAny
22 switch (field.type) {
23 case 'email': v = z.string().email(); break
24 case 'number': case 'scale': case 'rating': v = z.number(); break
25 case 'checkbox': v = z.boolean(); break
26 case 'multi_select': v = z.array(z.string()); break
27 default: v = z.string()
28 }
29 shape[field.id] = field.required ? v : v.optional()
30 }
31 return z.object(shape)
32}
33
34export async function POST(
35 req: NextRequest,
36 { params }: { params: Promise<{ slug: string }> }
37) {
38 const { slug } = await params
39 const body = await req.json()
40
41 const { data: form } = await supabase
42 .from('forms')
43 .select('id, fields, settings')
44 .eq('slug', slug)
45 .single()
46
47 if (!form || !form.settings?.is_published) {
48 return NextResponse.json({ error: 'Not found' }, { status: 404 })
49 }
50
51 const schema = buildZodSchema(form.fields)
52 const result = schema.safeParse(body)
53 if (!result.success) {
54 return NextResponse.json(
55 { error: result.error.flatten() },
56 { status: 400 }
57 )
58 }
59
60 await supabase.from('submissions').insert({
61 form_id: form.id,
62 data: result.data,
63 ip_address: req.headers.get('x-forwarded-for') ?? 'unknown',
64 })
65
66 await supabase
67 .from('forms')
68 .update({ submission_count: form.submission_count + 1 })
69 .eq('id', form.id)
70
71 return NextResponse.json({ message: form.settings.submit_message })
72}

Customization ideas

Add conditional logic

Show or hide fields based on previous answers by adding show_if rules to the field definition JSON.

Add form templates

Create a template gallery with pre-built forms (contact, survey, registration) users can clone and customize.

Add email notifications

Send form owner an email via Resend for each new submission with the response data formatted as a table.

Add file upload fields

For file field types, upload to Supabase Storage and store the URL in the submission data JSON.

Common pitfalls

Pitfall: Validating submissions only on the client side

How to avoid: Build a Zod schema dynamically from the form's field definitions in the API route and validate every submission server-side.

Pitfall: Storing field definitions as separate database rows instead of JSON

How to avoid: Store the complete field definition as a jsonb array in the forms table. This keeps the structure self-contained and order-preserving.

Pitfall: Not generating SEO metadata for public form pages

How to avoid: Use generateMetadata in the public form page to set the title and description from the form's title and description fields.

Best practices

  • Store form field definitions as a jsonb array to preserve order and simplify queries.
  • Build Zod validation schemas dynamically from field definitions at submission time for server-side validation.
  • Use generateMetadata on public form pages so shared links have proper titles and descriptions.
  • Queue prompts for the builder, renderer, and analytics as three separate V0 prompts.
  • Use NEXT_PUBLIC_SUPABASE_ANON_KEY for the client-side builder and SUPABASE_SERVICE_ROLE_KEY for the submission API.
  • Track form analytics (views, starts, completions) to measure form conversion rates.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a form builder with Next.js and Supabase. I need to dynamically generate a Zod validation schema from a JSON array of field definitions at submission time. Each field has id, type, label, required, and validation properties. Show me the function that maps field types to Zod validators and builds the complete schema.

Build Prompt

Build the dynamic form renderer component. Create a 'use client' FormRenderer that receives a fields JSON array and renders the correct shadcn/ui component for each field type. Map text to Input, select to Select, radio to RadioGroup, date to DatePicker, rating to a custom 5-star Button row, etc. Handle required validation with asterisks and client-side error messages.

Frequently asked questions

How does dynamic validation work?

The submission API reads the form's field definitions from the database and dynamically builds a Zod schema. Each field type maps to a Zod validator (string for text, number for ratings, email for email fields). Required flags and validation rules are applied automatically.

How many field types are supported?

Twelve: text, textarea, email, number, select, multi_select, checkbox, radio, date, file, rating, and scale. Each renders as the appropriate shadcn/ui component.

Can I embed forms on other websites?

Yes. Public forms at /forms/[slug] can be embedded in an iframe on any website. Add CORS headers if you need direct API submission from external domains.

What V0 plan do I need?

Premium ($20/month) is recommended for the builder's drag-and-drop interface and renderer iterations. Free tier works for simpler form pages.

How do I deploy?

Publish via V0's Share menu. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in Vars. Public form pages use generateMetadata for SEO.

Can RapidDev help build a custom form builder?

Yes. RapidDev has built 600+ apps including form platforms with conditional logic, file uploads, and payment collection. Book a free consultation to discuss your requirements.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.