# How to Build Resume builder backend with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

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.

## Before you start

- 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

## Step-by-step guide

### 1. 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.

```
// Paste this prompt into V0's AI chat:
// Build a resume builder. Create a Supabase schema with:
// 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)
// 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)
// 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)
// 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)
// 5. skills: id (uuid PK), resume_id (uuid FK), category (text), items (text[]), position (integer)
// RLS: users can only CRUD their own resumes and related data.
// 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.

### 2. 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.

```
// Paste this prompt into V0's AI chat:
// Build a resume editor at app/resumes/[id]/edit/page.tsx.
// Requirements:
// - shadcn/ui Tabs for sections: Personal, Experience, Education, Skills
// - Personal tab: Input fields for full_name, email, phone, location, linkedin, website. Textarea for summary.
// - Experience tab:
//   - List of experience entries, each in a Card
//   - 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)
//   - "Add Experience" Button, drag handle for reordering
//   - Dialog for adding new experience
// - Education tab: similar pattern with institution, degree, field, dates, gpa
// - Skills tab: grouped by category. Each group has category name Input and comma-separated items Input
// - Auto-save on blur via Server Actions: saveSection()
// - Right panel: live resume preview (rendered HTML matching the selected template)
// - Template Select at top to switch between templates (classic/modern/minimal)
// - 'use client' for the interactive editor, Server Actions for saves
```

**Expected 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.

### 3. 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.

```
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { renderToBuffer } from '@react-pdf/renderer'
import { ClassicTemplate } from '@/lib/resume-templates/classic'

export const maxDuration = 30

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params

  const { data: resume } = await supabase
    .from('resumes')
    .select('*, personal_info(*), experiences(*), education(*), skills(*)')
    .eq('id', id)
    .single()

  if (!resume) {
    return NextResponse.json({ error: 'Resume not found' }, { status: 404 })
  }

  const pdfBuffer = await renderToBuffer(
    ClassicTemplate({ resume })
  )

  const filePath = `pdfs/${id}/${Date.now()}.pdf`
  await supabase.storage.from('resumes').upload(filePath, pdfBuffer, {
    contentType: 'application/pdf',
  })

  return new NextResponse(pdfBuffer, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="${resume.personal_info?.full_name ?? 'resume'}.pdf"`,
    },
  })
}
```

**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.

### 4. 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.

```
// Paste this prompt into V0's AI chat:
// Build a resume list at app/resumes/page.tsx.
// Requirements:
// - Protected by auth
// - Grid of resume Cards showing:
//   - Preview thumbnail (small HTML render of the resume)
//   - Resume title, template name Badge, last updated date
//   - Star icon for primary resume (is_primary)
//   - DropdownMenu with actions: Edit, Download PDF, Duplicate, Delete
// - "Create New Resume" Button opens Dialog with title Input and template Select (classic/modern/minimal)
// - Server Actions: createResume(), duplicateResume(), deleteResume(), setPrimary()
// - Server Components for data fetching
// - Empty state with illustration and "Create Your First Resume" prompt
// - Use shadcn/ui Card, Badge, DropdownMenu, Dialog, Button
```

**Expected 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 example

File: `app/actions/resume.ts`

```typescript
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function saveSection(
  resumeId: string,
  section: string,
  data: Record<string, unknown>
) {
  const supabase = await createClient()

  switch (section) {
    case 'personal_info': {
      const { data: existing } = await supabase
        .from('personal_info')
        .select('id')
        .eq('resume_id', resumeId)
        .single()

      if (existing) {
        await supabase
          .from('personal_info')
          .update(data)
          .eq('resume_id', resumeId)
      } else {
        await supabase
          .from('personal_info')
          .insert({ resume_id: resumeId, ...data })
      }
      break
    }
    case 'experience':
      await supabase
        .from('experiences')
        .upsert({ resume_id: resumeId, ...data })
      break
    case 'education':
      await supabase
        .from('education')
        .upsert({ resume_id: resumeId, ...data })
      break
    case 'skills':
      await supabase
        .from('skills')
        .upsert({ resume_id: resumeId, ...data })
      break
  }

  await supabase
    .from('resumes')
    .update({ updated_at: new Date().toISOString() })
    .eq('id', resumeId)

  revalidatePath(`/resumes/${resumeId}/edit`)
}

export async function duplicateResume(resumeId: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  const { data: original } = await supabase
    .from('resumes')
    .select('*, personal_info(*), experiences(*), education(*), skills(*)')
    .eq('id', resumeId)
    .single()

  if (!original) return

  const { data: copy } = await supabase
    .from('resumes')
    .insert({
      user_id: user?.id,
      title: `${original.title} (Copy)`,
      template_id: original.template_id,
    })
    .select()
    .single()

  if (!copy) return
  revalidatePath('/resumes')
  redirect(`/resumes/${copy.id}/edit`)
}
```

## Common mistakes

- **Using browser-based PDF generation instead of server-side @react-pdf/renderer** — Browser PDF libraries (html2pdf, jsPDF) produce inconsistent output across devices and cannot access server-side data securely. Fix: Use @react-pdf/renderer in an API route (server-side). It produces consistent, high-quality PDFs with precise layout control and no browser dependency.
- **Not setting maxDuration for the PDF generation API route** — Rendering complex PDFs with multiple pages can exceed the default 10-second Vercel serverless timeout, causing generation to fail. Fix: Add export const maxDuration = 30 to the API route file to give the PDF renderer up to 30 seconds to complete.
- **Storing all resume data in a single JSONB column** — A single JSONB column makes it impossible to query, filter, or validate individual sections. Schema changes require migrating nested JSON. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/resume-builder-backend
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/resume-builder-backend
