SkillLink: The Interview Prep App That Turns an LLM Into a Structured Career Engine
It reads a resume, a self-description, and a job post, then turns them into questions, skill gaps, and a study plan the backend can actually trust.
- SkillLink’s main trick is not chat, but conversion: it turns messy career inputs into a durable prep artifact the backend can store and reuse.
- The repository treats Gemini like a constrained document engine, using schema-enforced output instead of freeform prose.
- Skill gaps are the product’s sharpest value, because they translate a job description into direction rather than generic feedback.
- The architecture is clean and modular, but the PDF generation path would need a worker-style upgrade before serious scale.
The app does not help you prep. It manufactures a prep artifact
Most interview tools promise guidance. SkillLink does something more specific: it produces a report. That report includes a match score, technical and behavioral questions, skill gaps, and a preparation plan that can be revisited later instead of regenerated from scratch.
That distinction matters. A chatty coach disappears after the session. A structured report becomes a reusable object, which is much closer to how serious product systems work.
Three inputs, one profile
SkillLink does not trust a single source of truth. It reads a resume PDF, asks the candidate to describe themselves, and takes in a target job description. Each one answers a different question: what you have done, how you frame yourself, and what the role actually wants.
| Input | What it contributes | Why it matters |
|---|---|---|
| Resume PDF | Past experience and credentials | Grounds the analysis in evidence |
| Self-description | Personal framing and confidence | Shows how the candidate positions themselves |
| Job description | Role requirements and priorities | Defines the target the report should optimize for |
The real trick is schema-enforced AI
The technical center of the repo is `Backend/src/services/ai.service.js`. Instead of asking Gemini for a blob of text and hoping for the best, the app defines the expected shape with Zod, converts that schema with `zod-to-json-schema`, and pushes the model toward valid JSON output.
const interviewReportSchema = z.object({
matchScore: z.number(),
technicalQuestions: z.array(z.string()),
behavioralQuestions: z.array(z.string()),
skillGaps: z.array(z.string()),
preparationPlan: z.array(z.string())
});
const jsonSchema = zodToJsonSchema(interviewReportSchema);
const response = await model.generateContent({
contents: prompt,
generationConfig: {
responseMimeType: 'application/json',
responseSchema: jsonSchema
}
});
That is the real product decision. SkillLink does not merely use an LLM. It constrains the model so the output can be parsed, stored, and reused as application data.
| Approach | Output shape | Reliability | Reuse |
|---|---|---|---|
| Freeform chat | Unstructured prose | Low | Poor |
| Schema-enforced AI | Validated JSON object | High | Strong |
| Template-only generation | Static document layout | Medium | Limited |
Skill gaps are the product, not the byproduct
This is where the app becomes genuinely useful. A generic assistant can tell you what you know. SkillLink tries to tell you what you are missing relative to a specific role, which is a much better fit for interview prep.
That directional logic changes the product from assessment to planning. The candidate is not just getting scored. They are getting an agenda.
| Question | Generic AI coach | SkillLink |
|---|---|---|
| What did I do well? | Usually yes | Yes, but inside a larger report |
| What am I missing? | Often vague | Explicit skill-gap output |
| What should I do next? | Informal advice | Day-by-day preparation plan |
| Can I save it? | Usually no | Yes, as a durable record |
Why the frontend stays calm
The frontend uses React Context and custom hooks inside `Frontend/src/features/interview/` to keep the feature self-contained. That matters because interview generation is a stateful workflow, and feature-local state is easier to reason about than a global app-wide tangle.
const { generateReport, reports, loading } = useInterview();
const handleSubmit = async (event) => {
event.preventDefault();
await generateReport(formData);
};
The pattern is not flashy. It is pragmatic. API calls, loading state, and interview-specific UI all live close together, which keeps the rest of the app cleaner.
The document pipeline is clever, and a little dangerous at scale
SkillLink also uses AI as a designer. In the resume-generation path, the model writes HTML, Puppeteer renders it, and the browser becomes a print engine. That is elegant because it removes an extra template layer and lets the model own the document layout directly.
It is also expensive. Puppeteer is a heavy tool for synchronous request flow, so this part of the system would probably want a background job or worker if traffic grew beyond prototype scale.
| Pipeline | Strength | Risk |
|---|---|---|
| AI writes HTML, Puppeteer prints PDF | Fast to prototype, flexible layout | Resource-heavy under load |
| Template engine renders PDF | Predictable and cheap | Less expressive |
| Background worker handles print jobs | Safer at scale | Adds queue and ops complexity |
What SkillLink gets right, and what it would need next
SkillLink’s strongest decision is the one that shapes everything else: it treats LLM output as structured product data. That unlocks persistence, reuse, and cleaner UI flows, which is a better foundation than a thin wrapper around a chat box.
Its next step is obvious. Move the expensive PDF work off the request path, and the architecture starts to look less like a clever demo and more like a system that can survive real usage.





