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
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
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.
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, scale6// 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.
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.
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 types5// - Center: sortable field list using dnd-kit. Each field shows: label, type Badge, required indicator6// - Right sidebar: field settings editor when a field is selected:7// - Input for label, Input for placeholder8// - Switch for required toggle9// - For select/radio/multi_select: Input list for options (add/remove)10// - For number/scale: min and max Input11// - For text/textarea: max length Input12// - Top bar: form title Input, "Preview" and "Settings" Tabs, Publish Switch13// - Tabs to switch between Builder (editor) and Preview (rendered form)14// - Settings tab: submit message Textarea, redirect URL Input, requires auth Switch15// - Auto-save: debounced Server Action that saves field definitions to the forms.fields jsonb16// - Use Card for the field palette items, Dialog for field settings on mobileExpected result: Users can drag field types, reorder them, configure properties, and see a live preview. Changes auto-save.
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.
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 slug5// - Use generateMetadata to set page title from form.title for SEO6// - If form is not published, show "This form is not available" message7// - Render a 'use client' FormRenderer component that maps each field in the fields jsonb to the corresponding shadcn/ui component:8// - text → Input9// - textarea → Textarea10// - email → Input type="email"11// - number → Input type="number"12// - select → Select with SelectItem for each option13// - multi_select → Checkbox group for each option14// - checkbox → Checkbox with label15// - radio → RadioGroup with RadioGroupItem for each option16// - date → Calendar/DatePicker17// - file → Input type="file"18// - rating → custom 5-star rating using Button components19// - scale → Slider or numbered Button row20// - Required fields show a red asterisk next to the label21// - Submit Button POSTs to /api/submit/[slug] and shows the success message from settings22// - Use Card to wrap the form with title and description at topExpected result: Public form pages render the correct UI component for each field type and submit responses to the API.
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.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { z } from 'zod'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)910type FieldDef = {11 id: string; type: string; label: string;12 required?: boolean; options?: string[];13 validation?: { min?: number; max?: number; maxLength?: number };14}1516function buildZodSchema(fields: FieldDef[]) {17 const shape: Record<string, z.ZodTypeAny> = {}1819 for (const field of fields) {20 let validator: z.ZodTypeAny2122 switch (field.type) {23 case 'email': validator = z.string().email(); break24 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 break29 case 'checkbox': validator = z.boolean(); break30 case 'multi_select': validator = z.array(z.string()); break31 case 'date': validator = z.string(); break32 default: validator = z.string()33 if (field.validation?.maxLength) validator = (validator as z.ZodString).max(field.validation.maxLength)34 }3536 shape[field.id] = field.required ? validator : validator.optional()37 }3839 return z.object(shape)40}4142export async function POST(43 req: NextRequest,44 { params }: { params: Promise<{ slug: string }> }45) {46 const { slug } = await params47 const body = await req.json()4849 const { data: form } = await supabase50 .from('forms')51 .select('id, fields, settings')52 .eq('slug', slug)53 .single()5455 if (!form || !form.settings.is_published) {56 return NextResponse.json({ error: 'Form not found' }, { status: 404 })57 }5859 const schema = buildZodSchema(form.fields)60 const result = schema.safeParse(body)6162 if (!result.success) {63 return NextResponse.json({ error: result.error.flatten() }, { status: 400 })64 }6566 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 })7172 await supabase.rpc('increment_submission_count', { form_id: form.id })7374 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.
Build the submission results and analytics page
Create the results page showing all submissions and field-level response analytics with charts.
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 submissions5// - Summary Cards: total submissions, submissions today, completion rate (if analytics tracked)6// - Table showing all submissions with columns dynamically generated from field labels7// - 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 Sheet9// - Analytics section: for select/radio/checkbox fields, show a Recharts BarChart of response distribution10// - For rating/scale fields, show average value and distribution histogram11// - For text fields, show a word cloud or most common responses12// - Tabs to switch between Table view and Analytics view13// - Export Button to download all submissions as CSV14// - Use Badge for field type indicators in table headersExpected result: The results page shows submissions in a dynamic table and field-level analytics with charts for select, rating, and scale fields.
Complete code
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { z } from 'zod'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)910type FieldDef = {11 id: string12 type: string13 label: string14 required?: boolean15 validation?: { min?: number; max?: number; maxLength?: number }16}1718function buildZodSchema(fields: FieldDef[]) {19 const shape: Record<string, z.ZodTypeAny> = {}20 for (const field of fields) {21 let v: z.ZodTypeAny22 switch (field.type) {23 case 'email': v = z.string().email(); break24 case 'number': case 'scale': case 'rating': v = z.number(); break25 case 'checkbox': v = z.boolean(); break26 case 'multi_select': v = z.array(z.string()); break27 default: v = z.string()28 }29 shape[field.id] = field.required ? v : v.optional()30 }31 return z.object(shape)32}3334export async function POST(35 req: NextRequest,36 { params }: { params: Promise<{ slug: string }> }37) {38 const { slug } = await params39 const body = await req.json()4041 const { data: form } = await supabase42 .from('forms')43 .select('id, fields, settings')44 .eq('slug', slug)45 .single()4647 if (!form || !form.settings?.is_published) {48 return NextResponse.json({ error: 'Not found' }, { status: 404 })49 }5051 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 }5960 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 })6566 await supabase67 .from('forms')68 .update({ submission_count: form.submission_count + 1 })69 .eq('id', form.id)7071 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.
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 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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation