Bismit Panda
Typescript June 3rd, 20256 min read

Generate Beautiful Resumes with React PDF and Next.js

BP
By Bismit Panda
Generate Beautiful Resumes with React PDF and Next.js
A dynamic, code-based resume generator that lets you version control your resume and export professional PDFs on demand.

While building my personal site, I wanted a smarter way to handle resumes, something version-controlled, customizable, and always up-to-date. That's when I decided to build a dynamic resume generator using Next.js and React PDF.

The Problem

Managing resumes is a pain. You have different versions for different roles, formatting gets messed up when you convert between file types, and keeping everything consistent across updates is tedious. I wanted something that would let me maintain my resume data in a structured format and generate professional PDFs on demand.

The Setup

I went with Next.js as I am using that for my profile

  • React PDF - For generating PDFs from React components
  • TypeScript - For type safety across the data structure

The core idea is simple: define your resume data once in a structured format, then render it as a PDF using React components.

File Structure

Data Structure

I started by defining a clear data structure for resume content:

lib/resume-data.ts

export interface ResumeData {
  personalInfo: {
    name: string;
    location: string;
    email: string;
    linkedin: string;
    github: string;
    website: string;
  };
  technologies: {
    languages: string;
    softwareAndFrameworks: string;
  };
  aboutMe: string;
  projects: Project[];
  experience: Experience[];
  education: Education[];
  achievements: Achievement[];
  certifications: Certification[];
}

I use content-collections to manage data and the types and data is fetched from there. Everything stays in sync because it's all referencing the same data source.

PDF Generation

The PDF generation happens through a Next.js API route at /resume. When you hit this endpoint, it renders a React PDF document and returns the binary data:

app/resume/route.tsx

export async function GET() {
  const buffer = await renderToBuffer(<Resume />);

  return new NextResponse(buffer, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": "inline; filename=resume.pdf",
      "Content-Length": buffer.length.toString(),
    },
  });
}

The Resume component is where the core rendering happens. It's a React PDF document that renders each section. The data is fetched from the resume-data.ts file.

app/resume/_components/resume.tsx

export function Resume() {
  return (
    <Document title="Bismit Panda's Resume" author="Bismit Panda" subject="Resume">
      <Page size="LETTER" style={styles.page}>
        <HeaderSection data={resumeData.personalInfo} />
        <AboutMeSection content={resumeData.aboutMe} />
        <ProjectsSection projects={resumeData.projects} />
        <ExperienceSection experience={resumeData.experience} />
        <TechnologiesSection technologies={resumeData.technologies} />
        <EducationSection education={resumeData.education} />
        <AchievementsSection achievements={resumeData.achievements} />
        <CertificationsSection certifications={resumeData.certifications} />
      </Page>
    </Document>
  );
}

Component Structure

Each section is its own component. Here's how the experience section works:

app/resume/_components/experience-section.tsx

export function ExperienceSection({ experience }: { experience: Experience[] }) {
  return (
    <View style={styles.section}>
      <Text style={styles.sectionTitle}>Experience</Text>
      {experience.map((exp, index) => (
        <View key={index} style={styles.entryContainer}>
          <View style={styles.entryHeader}>
            <Text style={styles.entryTitle}>
              {exp.title}, {exp.company}
            </Text>
            <Text style={styles.entryDate}>
              {formatDate(exp.startDate, "MMM yyyy")} -{" "}
              {exp.endDate ? formatDate(exp.endDate, "MMM yyyy") : "Present"}
            </Text>
          </View>
          <Text style={styles.paragraph}>{exp.description}</Text>
          <View style={styles.highlightsList}>
            {exp.highlights.map((highlight, highlightIndex) => (
              <View key={highlightIndex} style={styles.highlight}>
                <Text style={styles.bullet}></Text>
                <Text style={styles.highlightText}>{highlight.trim()}.</Text>
              </View>
            ))}
          </View>
        </View>
      ))}
    </View>
  );
}

Styling

Styling PDFs is different from web styling. It uses plain CSS-in-JS like styles. You define styles using StyleSheet.create() and apply them to components.

Here's a snippet of the styles I used:

app/resume/_components/styles.ts

export const styles = StyleSheet.create({
  page: {
    fontFamily: "Helvetica",
    fontSize: 10,
    lineHeight: 1.5,
    paddingTop: 25,
    paddingBottom: 25,
    paddingLeft: 30,
    paddingRight: 30,
    backgroundColor: "#ffffff",
  },
  sectionTitle: {
    fontSize: 12,
    fontWeight: "bold",
    marginBottom: 6,
    paddingBottom: 1,
    borderBottomWidth: 1,
    borderBottomColor: "#000000",
    borderBottomStyle: "solid",
  },
  // ... more styles
});

How It Works

Using it is straightforward:

  1. Update your resume data in resume-data.ts
  2. Navigate to http://localhost:3000/resume
  3. Your browser displays generated PDF (It may download automatically depending on your browser settings)

The PDF generates fresh each time, so any data changes are immediately reflected.

What I Learned

The biggest challenge was getting the layout right. React PDF's layout engine is powerful but different from CSS. Spending time understanding how flexbox works in this context saved me a lot of debugging later.

Data structure matters a lot. Having clean, typed interfaces made it easy to add new sections or modify existing ones without breaking anything.

Next Steps

This setup works great for my needs, but there are some obvious extensions which I may add later:

  • Multiple templates - Different layouts for different job types
  • Better styling - More sophisticated layouts and typography

The foundation is solid though. Having my resume as code means I can iterate quickly, keep everything in version control, and generate consistent PDFs whenever I need them.

Code