Building an AI Resume Analyzer with Serverless Technology
Introduction
Job hunting often fails silently—not because of skill gaps, but because resumes don’t align with Applicant Tracking Systems (ATS). To solve this, I built an AI Resume Analyzer that compares resumes against job descriptions and provides actionable feedback, ATS scores, and improvement tips.
What makes this project different is the architecture: no backend code, no infrastructure setup, and zero hosting cost. Everything runs on the frontend using Puter.js, a platform that brings authentication, storage, databases, and AI directly to the browser.
Project credit: Based on the work and teaching of @Adrian Hajdin / JavaScript Mastery on YouTube.
Discover this Project
GitHub Link: https://github.com/sheet848/ai-resume-analyzer
Project Overview: What Did I Build?
The AI Resume Analyzer is a full-stack application that:
- Accepts PDF resume uploads with drag-and-drop
- Matches resumes against job descriptions
- Generates comprehensive ATS scores (0-100)
- Provides AI feedback across 4 categories
- Stores all data securely in the cloud
Tech Stack:
- React 19 with React Router v7
- TypeScript for type safety
- Tailwind CSS v4 for styling
- Puter.js for backend services
- Claude Sonnet for AI analysis
Discovering Puter.js
The Traditional Backend Problem
Before discovering Puter.js, building an app like this meant; setting up authentication, database setup, file storage, AI integration, backend API. That’s easily 40-60 hours of work before writing any frontend code.
The Puter.js Revolution
With Puter.js, I added ONE script tag:
<script src="https://js.puter.com/v2/"></script>And suddenly I had access to:
- OAuth authentication (no configuration)
- Cloud file storage (unlimited)
- Key-value database (instant)
- Multiple AI models (free)
- All without a single line of backend code
This wasn’t just convenient—it fundamentally changed my development approach.
Building the Puter Wrapper: A Master Class in State Management
The Challenge
While Puter.js works perfectly with vanilla JavaScript, integrating it into React required careful consideration. Questions arose:
- How do I ensure Puter loads before my components try to use it?
- How do I share authentication state across components?
- How do I avoid prop drilling for Puter functions?
- How do I handle loading states elegantly?
The Solution: Zustand + Puter
I created a custom wrapper using Zustand that became the heart of the application:
export const usePuterStore = create<PuterStore>((set, get) => ({ isLoading: true, auth: { isAuthenticated: false, user: null },
init: async () => { const puter = getPuter(); if (puter) { await get().checkAuthStatus(); set({ isLoading: false }); } },
signIn: async () => { const puter = getPuter(); await puter.auth.signIn(); },
fs: { upload: async (files) => { const puter = getPuter(); return await puter.fs.upload(files); } } // ... more functions}));The Learning Curve
Creating this wrapper taught me:
When to wrap external libraries: Not every library needs wrapping. But when you need:
- Shared state across components
- Loading state management
- Type safety improvements
- Consistent error handling
A wrapper like this is invaluable.
Zustand vs Redux: Zustand is a simpler alternatives. No actions, no reducers—just functions that update state.
PDF Processing: The Technical Deep Dive
The Challenge
Displaying PDFs on the web is harder than it sounds. I needed to:
- Accept PDF uploads
- Display thumbnails on the homepage
- Allow full PDF viewing
- Keep file sizes reasonable
Solution: PDF.js + Canvas API
I built a custom conversion function:
const convertPDFToImage = async (file: File): Promise<File> => { // Load PDF const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument(arrayBuffer).promise;
// Get first page const page = await pdf.getPage(1);
// Render to canvas const canvas = document.createElement('canvas'); const context = canvas.getContext('2d');
const viewport = page.getViewport({ scale: 2 }); canvas.width = viewport.width; canvas.height = viewport.height;
await page.render({ canvasContext: context, viewport }).promise;
// Convert to PNG return new Promise((resolve) => { canvas.toBlob((blob) => { const imageFile = new File([blob], 'resume.png', { type: 'image/png' }); resolve(imageFile); }, 'image/png'); });};What I Learned:
Canvas API is powerful: Pixel manipulation. Render anything, convert to image, done.
PDF.js handles complexity: Loading multi-page PDFs, different formats, compression; all handled by the library.
Promises can nest: This function uses Promises within Promises. Understanding async flow is crucial.
React Router v7: The Data Loading Revolution
What’s New?
React Router v7 introduced a game-changer: route-level data loading.
Before (Manual Loading):
function ResumePage() { const [resume, setResume] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { loadResume().then(setResume).finally(() => setLoading(false)); }, [id]);
if (loading) return <Spinner />; return <ResumeView resume={resume} />;}After (Router Loading):
// Just define the loaderexport async function loader({ params }) { return await loadResume(params.id);}
// Component receives data as propsfunction ResumePage({ data }) { return <ResumeView resume={data} />;}Benefits:
- No loading state boilerplate: Router handles it
- Parallel data fetching: Multiple loaders run simultaneously
- Error boundaries: Built-in error handling
- Type safety: TypeScript knows the data shape
This pattern eliminated about 30% of my state management code.
Working with TypeScript
Coming from JavaScript, TypeScript felt like unnecessary overhead:
- Writing interfaces for everything
- Dealing with type errors
- More code for the same functionality
The Conversion Experience
This project changed my mind completely. Here’s why:
Caught bugs before runtime:
// TypeScript caught this immediatelyconst score = feedback.overalScore; // Typo!// Property 'overalScore' does not exist on type 'Feedback'// Did you mean 'overallScore'?Self-documenting code:
interface Feedback { overallScore: number; ats: ATSFeedback; toneAndStyle: CategoryFeedback;}
// I know exactly what this function expects and returnsfunction analyzeResume(resume: File): Promise<Feedback>Refactoring confidence: When I restructured the Feedback interface, TypeScript showed me every place that needed updating. No hunting through files.
Editor intelligence: My IDE knew what properties existed, what functions were available, and what types were expected.
Key Insight: TypeScript is about writing better code and not more code.
Key Takeaways
- Serverless frontend platforms can eliminate entire backend layers
- Zustand is a powerful, simpler alternative to Redux for many apps
- Structured AI prompting is non-negotiable for production use
- TypeScript pays off massively in medium-to-large projects
- Polish and UX details separate “working” from “professional”
Conclusion
This project reshaped how I approach modern web development. The backend isn’t always necessary, AI is a multiplier and the best portfolio projects solve real problems you personally care about.
The future of web development is already here: frontend-first, serverless, and AI-powered.
Resources
- Puter.js Docs: https://docs.puter.com
- React Router v7: https://reactrouter.com
- Tailwind CSS: https://tailwindcss.com