I Built a Free Business Card Generator with Next.js 14 - Here's What I Learned
Source: Dev.to

Last week, I launched FreeCard.app – a completely free business‑card and QR‑code generator. In this post I’ll share the journey, tech decisions, challenges, and lessons learned.
🎯 The Problem
I was frustrated with existing business‑card generators:
- ❌ Most require sign‑up just to see your design
- ❌ “Free” tools add watermarks to exports
- ❌ Premium features locked behind $10‑20 / month paywalls
- ❌ Overly complicated interfaces for a simple task
I wanted something that just works: enter a name, pick a template, download. Done.
💡 The Solution
FreeCard.app – a genuinely free business‑card generator with:
- ✅ 25+ professional templates
- ✅ Automatic QR‑code generation (vCard format)
- ✅ Custom colours and fonts
- ✅ PNG, PDF, and vCard export
- ✅ Email‑signature generator
- ✅ No sign‑up required
- ✅ No watermarks, ever
Business model? Simple – Google AdSense. No premium tier, no upsells.
🛠️ Tech Stack
| Layer | Technology |
|---|---|
| Frontend | Next.js 14 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS + shadcn/ui |
| State | Zustand |
| Database | MongoDB + Mongoose |
| Auth | NextAuth.js v5 |
| Export | html-to-image + jsPDF |
| QR | qrcode.react |
| Hosting | Vercel |
Why This Stack?
- Next.js 14 App Router – Server components by default = faster initial load. The new router is production‑ready.
- Zustand over Redux – For a tool like this, Zustand’s simplicity wins. No boilerplate, just works:
// store/useCardStore.ts
import { create } from 'zustand';
interface CardStore {
card: CardData;
setField: (field: string, value: string) => void;
setTemplate: (templateId: string) => void;
}
export const useCardStore = create((set) => ({
card: initialCard,
setField: (field, value) =>
set((state) => ({ card: { ...state.card, [field]: value } })),
setTemplate: (templateId) =>
set((state) => ({ card: { ...state.card, template: templateId } })),
}));
- shadcn/ui – Not a component library, but a collection of copy‑paste components. Full control, great defaults, accessible out of the box.
🎨 The Template System
One of the trickiest parts was building a flexible template system. Each template must:
- Accept the same props (name, title, colours, etc.)
- Render differently based on design
- Be exportable as PNG/PDF
A simplified example:
// templates/ModernDark.tsx
interface TemplateProps {
card: CardData;
showQR?: boolean;
}
export function ModernDark({ card, showQR = true }: TemplateProps) {
const { fullName, title, email, phone, colors } = card;
return (
<div style={{ backgroundColor: colors?.background ?? '#fff' }}>
<h1>{fullName || 'Your Name'}</h1>
<h2>{title || 'Your Title'}</h2>
{/* …more fields… */}
{showQR && <QRCode value={generateVCard(card)} />}
</div>
);
}
📤 The Export Challenge
Exporting HTML to PNG/PDF sounds simple until you actually try it. Here’s what worked for me.
PNG Export
import { toPng } from 'html-to-image';
export async function exportToPNG(elementId: string) {
const element = document.getElementById(elementId);
if (!element) throw new Error('Element not found');
const dataUrl = await toPng(element, {
quality: 1,
pixelRatio: 3, // High resolution
backgroundColor: '#ffffff',
});
// Trigger download
const link = document.createElement('a');
link.download = 'business-card.png';
link.href = dataUrl;
link.click();
}
PDF Export (Standard Business‑Card Size)
import jsPDF from 'jspdf';
import { toPng } from 'html-to-image';
export async function exportToPDF(elementId: string) {
const element = document.getElementById(elementId);
const dataUrl = await toPng(element, { pixelRatio: 3 });
// Standard business card: 3.5" × 2" (88.9 mm × 50.8 mm)
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: [88.9, 50.8],
});
pdf.addImage(dataUrl, 'PNG', 0, 0, 88.9, 50.8);
pdf.save('business-card.pdf');
}
vCard Generation
QR codes encode vCard data so people can scan and save contacts:
export function generateVCard(card: CardData): string {
return [
'BEGIN:VCARD',
'VERSION:3.0',
`FN:${card.fullName}`,
`ORG:${card.company || ''}`,
`TITLE:${card.title || ''}`,
`EMAIL:${card.email || ''}`,
`TEL:${card.phone || ''}`,
`URL:${card.website || ''}`,
'END:VCARD',
].join('\n');
}
🐛 Challenges & Solutions
Fonts Not Rendering in Export
Problem: Custom fonts fell back to system fonts in PNG exports.
Solution: Ensure fonts are fully loaded before calling toPng:
await document.fonts.ready;
await new Promise((resolve) => setTimeout(resolve, 100)); // tiny delay
const dataUrl = await toPng(element);
QR Code Sizing
Problem: QR codes appeared blurry or were cut off at high resolutions.
Solution: Render the QR code at a larger size (pixelRatio = 3) and then scale it down in the PDF/PNG export. This preserves crisp edges while keeping the visual footprint small.
Color Picker Performance
Problem: Real‑time preview lagged with continuous color changes.
Solution: Debounced updates.
const debouncedSetColor = useMemo(
() => debounce((color: string) => setColor('primary', color), 50),
[]
);
📊 Results (First Week)
- 🚀 Launched on Product Hunt
- 📈 500+ unique visitors
- 💾 200+ cards created
- 🔗 Backlinks from BetaList, AlternativeTo
- 💰 $0 revenue (AdSense pending approval)
💸 Cost Breakdown
| Item | Monthly Cost |
|---|---|
| Vercel Hosting | $0 (Hobby) |
| MongoDB Atlas | $0 (Free tier) |
| Domain (.app) | ~ $1.25 (≈ $15/yr) |
| Total | ~ $1.25 |
That’s it—a fully functional SaaS‑like product for about $15/year.
🎓 Lessons Learned
- Ship Fast, Iterate Later – Launched with 5 templates; now there are 25. The first version was “good enough”; users told me what they actually wanted.
- “Free” is a Feature – In a market full of “freemium” tools with aggressive upsells, being genuinely free is a differentiator.
- Zustand > Redux for Small Projects – If your state fits in one file, you don’t need Redux. Zustand’s API is a joy to use.
- shadcn/ui is Amazing – Copy‑paste components mean you own the code. No fighting with library abstractions.
- The App Router is Ready – After months of “should I use Pages or App Router?” – just use App Router. It’s stable and the DX is great.
🔮 What’s Next
- More templates (targeting 50+)
- NFC business‑card support
- Public shareable links (
freecard.app/c/username) - LinkedIn profile import
- Template marketplace (user‑submitted designs)
🙏 Try It Out
If you need a business card, give it a try:
No signup, no watermarks, no BS. Just free business cards.
What do you think? I’d love to hear your feedback in the comments. What features would make this more useful for you?
If you found this useful, consider:
- ⭐ Upvoting on Product Hunt
- 🐦 Following me on Twitter for more indie‑hacker content
- 📧 Sharing with someone who needs business cards
Tags
nextjs typescript react webdev opensource indiehacker buildinpublic sideproject
