Build a resume builder backend with V0 using Next.js, Supabase, and @react-pdf/renderer that lets job seekers enter their experience, education, and skills into structured forms and export polished, ATS-friendly PDF resumes from multiple templates — all in about 1-2 hours.
What you're building
Job seekers need clean, professional resumes that pass Applicant Tracking Systems (ATS). Most resume builders are subscription-based and lock your data. Building your own gives users full control over their resume data and unlimited PDF exports.
V0 generates the structured resume editor, template preview, and PDF generation pipeline from prompts. The key technical challenge is making the PDF output match the visual preview — @react-pdf/renderer runs server-side in an API route with the same layout logic as the React preview component.
The architecture uses Next.js App Router with Server Components for the resume list, a client component for the interactive editor and live preview, an API route for PDF generation with @react-pdf/renderer, Server Actions for saving resume sections, and Supabase Storage for persisting generated PDFs.
Final result
A resume builder with structured data entry, live template preview, multiple template options, server-side PDF generation, and persistent storage for resumes and exported files.
Tech stack
Prerequisites
- A V0 account (Premium recommended for the editor complexity)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Your own resume data for testing the builder
Build steps
Set up the project and resume schema
Open V0 and create a new project. Use the Connect panel to add Supabase. Create the normalized schema for resumes with separate tables for personal info, experiences, education, and skills.
1// Paste this prompt into V0's AI chat:2// Build a resume builder. Create a Supabase schema with:3// 1. resumes: id (uuid PK), user_id (uuid FK), title (text), template_id (text), is_primary (boolean default false), created_at (timestamptz), updated_at (timestamptz)4// 2. personal_info: id (uuid PK), resume_id (uuid FK unique), full_name (text), email (text), phone (text), location (text), linkedin (text), website (text), summary (text)5// 3. experiences: id (uuid PK), resume_id (uuid FK), company (text), title (text), start_date (date), end_date (date nullable), is_current (boolean), description (text), achievements (text[]), position (integer)6// 4. education: id (uuid PK), resume_id (uuid FK), institution (text), degree (text), field (text), start_date (date), end_date (date nullable), gpa (text), position (integer)7// 5. skills: id (uuid PK), resume_id (uuid FK), category (text), items (text[]), position (integer)8// RLS: users can only CRUD their own resumes and related data.9// Generate SQL migration and TypeScript types.Pro tip: Use V0's prompt queuing — queue the schema prompt first, then immediately queue the editor and PDF generation prompts while the schema generates.
Expected result: Supabase is connected with all five tables created. RLS policies ensure users can only access their own resume data.
Build the section-by-section resume editor
Create the resume editor page with Tabs for each section — personal info, experience, education, and skills. Each tab has structured form fields that save via Server Actions.
1// Paste this prompt into V0's AI chat:2// Build a resume editor at app/resumes/[id]/edit/page.tsx.3// Requirements:4// - shadcn/ui Tabs for sections: Personal, Experience, Education, Skills5// - Personal tab: Input fields for full_name, email, phone, location, linkedin, website. Textarea for summary.6// - Experience tab:7// - List of experience entries, each in a Card8// - Each Card: Input for company and title, date pickers for start/end, Checkbox for is_current (hides end date), Textarea for description, dynamic text[] Input for achievements (add/remove)9// - "Add Experience" Button, drag handle for reordering10// - Dialog for adding new experience11// - Education tab: similar pattern with institution, degree, field, dates, gpa12// - Skills tab: grouped by category. Each group has category name Input and comma-separated items Input13// - Auto-save on blur via Server Actions: saveSection()14// - Right panel: live resume preview (rendered HTML matching the selected template)15// - Template Select at top to switch between templates (classic/modern/minimal)16// - 'use client' for the interactive editor, Server Actions for savesExpected result: A tabbed resume editor with structured forms for each section. Changes auto-save on blur. A live preview panel on the right shows how the resume will look.
Create the PDF generation API route
Build the API route that generates a PDF from the resume data using @react-pdf/renderer. The PDF uses the same layout logic as the preview component, ensuring what you see matches what you get.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { renderToBuffer } from '@react-pdf/renderer'4import { ClassicTemplate } from '@/lib/resume-templates/classic'56export const maxDuration = 3078const supabase = createClient(9 process.env.SUPABASE_URL!,10 process.env.SUPABASE_SERVICE_ROLE_KEY!11)1213export async function GET(14 req: NextRequest,15 { params }: { params: Promise<{ id: string }> }16) {17 const { id } = await params1819 const { data: resume } = await supabase20 .from('resumes')21 .select('*, personal_info(*), experiences(*), education(*), skills(*)')22 .eq('id', id)23 .single()2425 if (!resume) {26 return NextResponse.json({ error: 'Resume not found' }, { status: 404 })27 }2829 const pdfBuffer = await renderToBuffer(30 ClassicTemplate({ resume })31 )3233 const filePath = `pdfs/${id}/${Date.now()}.pdf`34 await supabase.storage.from('resumes').upload(filePath, pdfBuffer, {35 contentType: 'application/pdf',36 })3738 return new NextResponse(pdfBuffer, {39 headers: {40 'Content-Type': 'application/pdf',41 'Content-Disposition': `attachment; filename="${resume.personal_info?.full_name ?? 'resume'}.pdf"`,42 },43 })44}Expected result: GET /api/resumes/[id]/pdf generates a PDF using @react-pdf/renderer, stores it in Supabase Storage, and returns it as a downloadable file.
Build the resume list page with template switching
Create the main resumes page showing all of the user's resumes. Each resume has a Card with a preview thumbnail, template info, and actions for editing, downloading, duplicating, and deleting.
1// Paste this prompt into V0's AI chat:2// Build a resume list at app/resumes/page.tsx.3// Requirements:4// - Protected by auth5// - Grid of resume Cards showing:6// - Preview thumbnail (small HTML render of the resume)7// - Resume title, template name Badge, last updated date8// - Star icon for primary resume (is_primary)9// - DropdownMenu with actions: Edit, Download PDF, Duplicate, Delete10// - "Create New Resume" Button opens Dialog with title Input and template Select (classic/modern/minimal)11// - Server Actions: createResume(), duplicateResume(), deleteResume(), setPrimary()12// - Server Components for data fetching13// - Empty state with illustration and "Create Your First Resume" prompt14// - Use shadcn/ui Card, Badge, DropdownMenu, Dialog, ButtonExpected result: A grid of resume Cards with preview thumbnails, template Badges, and action menus. Users can create, duplicate, download, and delete resumes from this page.
Complete code
1'use server'23import { createClient } from '@/lib/supabase/server'4import { revalidatePath } from 'next/cache'5import { redirect } from 'next/navigation'67export async function saveSection(8 resumeId: string,9 section: string,10 data: Record<string, unknown>11) {12 const supabase = await createClient()1314 switch (section) {15 case 'personal_info': {16 const { data: existing } = await supabase17 .from('personal_info')18 .select('id')19 .eq('resume_id', resumeId)20 .single()2122 if (existing) {23 await supabase24 .from('personal_info')25 .update(data)26 .eq('resume_id', resumeId)27 } else {28 await supabase29 .from('personal_info')30 .insert({ resume_id: resumeId, ...data })31 }32 break33 }34 case 'experience':35 await supabase36 .from('experiences')37 .upsert({ resume_id: resumeId, ...data })38 break39 case 'education':40 await supabase41 .from('education')42 .upsert({ resume_id: resumeId, ...data })43 break44 case 'skills':45 await supabase46 .from('skills')47 .upsert({ resume_id: resumeId, ...data })48 break49 }5051 await supabase52 .from('resumes')53 .update({ updated_at: new Date().toISOString() })54 .eq('id', resumeId)5556 revalidatePath(`/resumes/${resumeId}/edit`)57}5859export async function duplicateResume(resumeId: string) {60 const supabase = await createClient()61 const { data: { user } } = await supabase.auth.getUser()6263 const { data: original } = await supabase64 .from('resumes')65 .select('*, personal_info(*), experiences(*), education(*), skills(*)')66 .eq('id', resumeId)67 .single()6869 if (!original) return7071 const { data: copy } = await supabase72 .from('resumes')73 .insert({74 user_id: user?.id,75 title: `${original.title} (Copy)`,76 template_id: original.template_id,77 })78 .select()79 .single()8081 if (!copy) return82 revalidatePath('/resumes')83 redirect(`/resumes/${copy.id}/edit`)84}Customization ideas
Add AI-powered content suggestions
Use the OpenAI API to suggest improved bullet points for experience descriptions based on the job title and industry.
Build a cover letter generator
Add a cover letter tab that generates personalized cover letters using the resume data and a job description input.
Add more templates
Create additional PDF templates (creative, executive, academic) with different layouts, fonts, and color schemes using @react-pdf/renderer styles.
Build a job application tracker
Add a page to track which resume was sent to which company, application status, and follow-up dates.
Common pitfalls
Pitfall: Using browser-based PDF generation instead of server-side @react-pdf/renderer
How to avoid: Use @react-pdf/renderer in an API route (server-side). It produces consistent, high-quality PDFs with precise layout control and no browser dependency.
Pitfall: Not setting maxDuration for the PDF generation API route
How to avoid: Add export const maxDuration = 30 to the API route file to give the PDF renderer up to 30 seconds to complete.
Pitfall: Storing all resume data in a single JSONB column
How to avoid: Use normalized tables (personal_info, experiences, education, skills) with proper types and constraints. This enables section-level queries and validation.
Best practices
- Use @react-pdf/renderer in API routes for consistent, high-quality PDF output without browser dependencies
- Set maxDuration = 30 in the PDF API route for Vercel serverless to handle complex multi-page resumes
- Store generated PDFs in Supabase Storage for re-download without regeneration
- Use V0's prompt queuing to generate the editor, preview, and PDF template in rapid sequence
- Normalize resume data into separate tables (personal_info, experiences, education, skills) for clean queries and validation
- Auto-save on blur in the editor so users never lose progress
AI prompts to try
Copy these prompts to build this project faster.
I'm building a resume builder with Next.js App Router and @react-pdf/renderer. Write a React PDF template component that renders a professional resume with sections for personal info (name, email, phone centered), summary paragraph, experience entries (company, title, dates, bullet points), education (institution, degree, dates), and skills (grouped by category). Use @react-pdf/renderer's Document, Page, View, Text, and StyleSheet. Include proper typography and spacing.
Create a live resume preview component that renders an HTML representation of the resume matching a selected template. Accept resume data as props (personal_info, experiences, education, skills) and template_id. Render with proper typography, section headers, bullet points, and date formatting. This is the 'what you see is what you get' preview that matches the @react-pdf/renderer output.
Frequently asked questions
What V0 plan do I need for a resume builder?
V0 Free works for the basic build, but Premium ($20/month) is recommended because the editor has multiple complex tabs and a live preview that benefit from prompt queuing.
Will the PDF match the visual preview exactly?
Yes, if you use shared style constants between the React preview component and the @react-pdf/renderer template. Define font sizes, spacing, and colors in a shared config file used by both.
Are the generated PDFs ATS-friendly?
@react-pdf/renderer produces text-based PDFs (not images), so ATS systems can parse the content. Stick to standard fonts, avoid complex layouts, and use proper heading hierarchy for best ATS compatibility.
How do I add more resume templates?
Create new @react-pdf/renderer components in lib/resume-templates/ with different layouts and styles. Add the template ID to the Select options in the editor. The PDF route dynamically loads the template based on template_id.
How do I deploy the resume builder?
Click Share then Publish to Production in V0. Set SUPABASE_SERVICE_ROLE_KEY in the Vars tab without NEXT_PUBLIC_ prefix. PDF generation runs server-side so no special browser requirements.
Can RapidDev help build a custom resume builder?
Yes. RapidDev has built 600+ apps including resume platforms with AI content suggestions, custom template engines, and job application tracking. Book a free consultation to discuss your needs.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation