ankurdotio/interview-ai-yt: How to Turn an LLM Into a Structured Interview Engine
A look at the schema-first design that powers tailored questions, prep roadmaps, and ATS-friendly resume PDFs without letting the model run wild.

Built an AI tool that generates personalized interview questions based on any job description using GPT-3.5.
- This repo matters because it makes the schema the product boundary, so the model can feed UI, storage, and PDFs without turning into a blob of text.
- Its strongest move is not interview generation alone, but a pipeline that turns one prompt into structured report data and a printable resume artifact.
- The codebase feels production-minded because it pairs Zod validation, nested MongoDB schemas, and Puppeteer rendering instead of relying on a thin prompt wrapper.
- It sits between a demo and a SaaS platform: lean enough to self-host, but disciplined enough to build real downstream features on top of AI output.
Most AI interview tools stop at generated text. This repo keeps going, because it treats the model like a backend service that has to obey contracts. That one decision changes everything: the app can store the output, render it, score it, and turn part of it into a PDF without brittle cleanup code.
The real product is the schema
The repo’s most interesting move is simple. It forces Gemini into a structured shape with zod and JSON schema conversion, then uses that shape as the source of truth for the rest of the app. The model is not chatting. It is filling fields the backend can trust.
const reportSchema = z.object({
matchScore: z.number(),
technicalQuestions: z.array(z.object({
question: z.string(),
answer: z.string().optional(),
difficulty: z.string().optional()
})),
preparationPlan: z.array(z.object({
day: z.number(),
focus: z.string(),
tasks: z.array(z.string())
}))
});
const jsonSchema = zodToJsonSchema(reportSchema, 'InterviewReport');
const result = await gemini.generateContent({
contents: prompt,
generationConfig: { responseMimeType: 'application/json' },
responseSchema: jsonSchema
});
That structure matters because the frontend is built to consume it. A report card can map over a nested array. A roadmap can render day by day. A database record can persist the whole object without guessing what a response means. The product gets deterministic behavior out of an otherwise probabilistic system.
The interview report model is the backbone
The report model is where the app stops being a prompt demo. Nested schemas for technical questions, behavioral prompts, and the prep plan let the backend save a multi-part interview artifact instead of a single blob. That makes the product feel like a reporting system, not a chatbot.
It also explains the UI shape. Once the data is nested and predictable, the frontend can build cards, sections, and timelines without special cases. The result is boring in the best possible way: stable data in, stable interface out.
The PDF pipeline is the cleverest stretch
The resume flow is the repo’s smartest stretch beyond interviews. The backend asks the model for HTML and CSS, then hands that markup to Puppeteer for rendering. That is a serious choice: it uses a browser as a layout engine, which is much more faithful than trying to fake typography with a low-level PDF library.
const html = await ai.generateResumeHtml(profile, jobDescription);
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
That pipeline matters because it lets the AI handle copy and layout together while still landing in a medium people trust. A browser can honor spacing, hierarchy, and pagination. A plain text generator cannot.
Security and state are pragmatic, not flashy
The rest of the stack is plain-spoken and sensible. JWT handles authentication, a token blacklist tightens logout behavior, and React Context carries interview state through the frontend. None of that is novel, but it is the right kind of mundane for a product that needs to hold together around a risky AI core.
That balance is the point. The repo is not trying to impress with infrastructure theater. It uses enough discipline to keep the generated output from leaking into the rest of the app as chaos.
How it compares to interview prep tools
| Dimension | ankurdotio/interview-ai-yt | Typical commercial SaaS | Typical prompt wrapper |
|---|---|---|---|
| Input | Job description text or URL | Resume plus job description | Usually just pasted text |
| Output structure | Structured report data plus PDF | Multi-feature product surface | Freeform text or thin JSON |
| Persistence | MongoDB interview reports | Cloud account history and analytics | Often none or ad hoc storage |
| Resume generation | HTML/CSS through Puppeteer | Branded document workflows | Usually missing |
| Deployment | Self-hosted or local | Hosted subscription service | Self-hosted or local |
| What you get | A buildable system with contracts | A polished service layer | A demo that needs hardening |
That middle position is the interesting one. It is lighter than a commercial platform, but much more serious than a prompt toy. You can see the product thinking without paying for a lot of platform overhead.
What this repo teaches builders
The lesson is not “use Gemini” or “use Puppeteer.” It is that LLM apps get much more useful when the model is forced into contracts the rest of the stack can trust. Once you do that, UI, storage, and document generation stop being separate problems and start becoming one product pipeline.

This project helps job seekers practice for interviews by simulating real-world scenarios based on actual job listings.
That is why this repo is worth studying. It shows how to turn a model from a source of text into a source of application data, and that shift is where many practical AI products begin.