DEV Community

Pravin Tripathi
Pravin Tripathi

Posted on

Building an AI-Powered Job Matcher with Nuxt 4 (That Doesn't Auto-Apply)

After getting zero responses from 200+ auto-applied job applications, I decided to build something different. Here's how I created an AI-powered job matching system that helps you find relevant opportunities while keeping you in control.

The Problem with Auto-Apply

We've all seen them - tools that promise to "apply to 100 jobs while you sleep." But here's what actually happens:

  • Generic applications that don't stand out
  • No customization for specific roles
  • Recruiters can smell spray-and-pray a mile away
  • Zero personal touch = zero interviews

I tracked my own job search and found:

  • Auto-applied: 200+ applications → 0 interviews
  • Manual (targeted): 15 applications → 4 interviews

Quality beats quantity. Every time.

The Solution: AI Discovery, Human Decision

I built a job portal with a different philosophy:

AI handles: Finding and scoring relevant jobs
Human handles: Reviewing and applying

Let's break down how I built it.

Tech Stack

  • Frontend: Nuxt 4 + Vue 3 + TypeScript + Tailwind CSS 4
  • Backend: Nitro server + SQLite (better-sqlite3)
  • AI: Claude Haiku via CLI
  • Scraping: Playwright + Chromium

Architecture Overview

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Job Sites     │────▶│    Scrapers     │────▶│    SQLite DB    │
│ (Naukri/LinkedIn)│     │  (Playwright)   │     │                 │
└─────────────────┘     └─────────────────┘     └────────┬────────┘
                                                         │
┌─────────────────┐     ┌─────────────────┐              │
│   Vue Frontend  │◀────│   AI Matcher    │◀─────────────┘
│    (Nuxt 4)     │     │ (Claude Haiku)  │
└─────────────────┘     └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

Step 1: Scraping Jobs with Playwright

First, I created a base scraper class:

// server/scraper/sites/base.ts
export abstract class BaseScraper {
  protected browser: Browser | null = null;

  abstract getJobListSelector(): string;
  abstract buildSearchUrl(keywords: string[]): string;
  abstract parseJobCard(element: ElementHandle): Promise<ParsedJob>;

  async scrape(keywords: string[]): Promise<Job[]> {
    await this.initBrowser();
    const url = this.buildSearchUrl(keywords);
    // Navigate, wait for selector, parse cards...
  }
}
Enter fullscreen mode Exit fullscreen mode

Each site extends this base:

// server/scraper/sites/naukri.ts
export class NaukriScraper extends BaseScraper {
  getJobListSelector() {
    return '.srp-jobtuple-wrapper';
  }

  buildSearchUrl(keywords: string[]) {
    return `https://www.naukri.com/${keywords.join('-')}-jobs`;
  }

  async parseJobCard(element: ElementHandle) {
    // Extract title, company, location, etc.
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: The Matching Algorithm

Here's where it gets interesting. I use a weighted scoring system:

const WEIGHTS = {
  techStack: 0.40,      // 40% - Does the job need your skills?
  roleAlignment: 0.25,  // 25% - Is this the type of role you want?
  experienceLevel: 0.20, // 20% - Are you over/under qualified?
  domainRelevance: 0.15  // 15% - Have you worked in this industry?
};
Enter fullscreen mode Exit fullscreen mode

The AI (Claude Haiku) evaluates each job:

// Simplified matching logic
async function matchJob(job: Job, profile: UserProfile): Promise<number> {
  const prompt = `
    Score this job match (0-100):

    Job: ${job.title} at ${job.company}
    Requirements: ${job.requirements}

    Candidate Profile:
    - Experience: ${profile.yearsOfExperience} years
    - Tech Stack: ${profile.techStack.join(', ')}
    - Target Roles: ${profile.targetRoles.join(', ')}

    Scoring weights:
    - Tech Stack Match: 40%
    - Role Alignment: 25%
    - Experience Level: 20%
    - Domain Relevance: 15%

    Return only the score as a number.
  `;

  const score = await callClaude(prompt);
  return parseInt(score);
}
Enter fullscreen mode Exit fullscreen mode

Jobs scoring ≥50 are marked as "matched" (worth reviewing).
Jobs scoring <50 are marked as "ignored" (not a good fit).

Step 3: The User Flow

new → matched → interested → applied → archived
         ↓
      ignored
Enter fullscreen mode Exit fullscreen mode
  1. Upload Resume: AI extracts your profile (experience, skills, target roles)
  2. Scrape Jobs: Playwright fetches listings from job sites
  3. Match: AI scores each job against your profile
  4. Review: You see matched jobs sorted by score
  5. Apply: Click "View Original" → Apply manually on the company site
  6. Track: Mark as "applied" to track your pipeline

Step 4: Keeping Costs Low

Claude Haiku is incredibly cheap:

Activity Tokens Cost
Resume upload ~15K One-time
Match 20 jobs ~25K ~$0.0075
Monthly total ~750K $0.22

That's essentially free if you have Claude Pro.

The Nuxt 4 Experience

Some things I loved about Nuxt 4:

Server Routes are magic:

// server/api/match.post.ts
export default defineEventHandler(async (event) => {
  const jobs = await getUnmatchedJobs();
  const scores = await matchJobs(jobs);
  return { matched: scores.filter(s => s >= 50).length };
});
Enter fullscreen mode Exit fullscreen mode

Composables for state:

// composables/useJobs.ts
export const useJobs = () => {
  const jobs = useState<Job[]>('jobs', () => []);

  const fetchJobs = async (status?: string) => {
    jobs.value = await $fetch('/api/jobs', {
      query: { status }
    });
  };

  return { jobs, fetchJobs };
};
Enter fullscreen mode Exit fullscreen mode

What I Deliberately Didn't Build

This is important. These features are out of scope by design:

  • ❌ Auto-apply to jobs
  • ❌ Form auto-fill
  • ❌ Automated emails to recruiters
  • ❌ Batch applications

Why? Because the best job applications are personal. AI should augment your job search, not replace your judgment.

Try It Yourself

The project is open source:

GitHub: https://github.com/1291pravin/job-hunt-ai

Setup:

git clone https://github.com/1291pravin/job-hunt-ai
cd job-portal-app
npm install
npx playwright install chromium
npm run dev
Enter fullscreen mode Exit fullscreen mode

What's Next?

Looking for contributors to help with:

  • More job sources (Indeed, Glassdoor, AngelList)
  • Better matching with ML models
  • Resume tailoring suggestions
  • Interview prep notes feature

Conclusion

Sometimes the best feature is the one you don't build. By keeping humans in the loop, this tool has helped me land more interviews with fewer applications.

The code is MIT licensed. Fork it, improve it, and let me know what you think!

Top comments (0)