Build an AI Mock Interview Platform with Next.js and Gemini
Step-by-step tutorial to build an AI-powered interview simulator with voice input, code evaluation, and personalized scorecards using Google's Gemini API.
Build an AI Mock Interview Platform with Next.js and Gemini
Technical interview preparation is stressful. AI can simulate realistic interviews, ask follow-up questions, and evaluate answers. In this tutorial, you’ll build a complete mock interview platform with voice support and code assessment using Next.js 15 and Google Gemini.
Key Takeaways
- Integrate Gemini 2.0 for conversational AI interviews
- Add voice input with Web Speech API
- Evaluate code solutions automatically
- Generate performance reports and improvement tips
Prerequisites
- Node.js 20+
- Google AI Studio API key (free tier available)
- Basic Next.js and TypeScript knowledge
Step 1: Project Setup
npx create-next-app@latest ai-interview-platform --typescript --tailwind --app
cd ai-interview-platform
npm install @google/generative-ai zod react-speech-kit
npm install -D prisma @prisma/client
npx prisma init
Step 2: Database Schema (Prisma)
prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Interview {
id String @id @default(cuid())
userId String
role String // "frontend", "backend", "fullstack"
difficulty String // "junior", "mid", "senior"
questions Json // Array of question objects
answers Json // Array of answer objects
scores Json // Evaluation scores per question
finalScore Float?
completedAt DateTime?
createdAt DateTime @default(now())
}
model Question {
id String @id @default(cuid())
text String
category String // "algorithms", "system-design", "react"
difficulty String
sampleAnswer String
hints String[]
createdAt DateTime @default(now())
}
Run migration: npx prisma migrate dev --name init
Step 3: Gemini Integration
Create lib/gemini.ts:
import { GoogleGenerativeAI } from '@google/generative-ai';
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
export const interviewModel = genAI.getGenerativeModel({
model: 'gemini-2.0-flash-exp',
});
const INTERVIEW_SYSTEM_PROMPT = `
You are a technical interviewer conducting a {difficulty} level {role} interview.
Rules:
1. Ask one question at a time
2. Wait for the candidate's answer
3. Ask relevant follow-up questions based on their response
4. Evaluate answers for correctness, communication, and depth
5. After 5 questions, provide a summary score and feedback
Keep responses conversational but professional.
`;
export async function generateQuestion(
role: string,
difficulty: string,
previousContext: string[]
): Promise<string> {
const prompt = `
${INTERVIEW_SYSTEM_PROMPT.replace('{role}', role).replace('{difficulty}', difficulty)}
Previous questions and answers: ${previousContext.join('\n')}
Generate the next technical interview question. Focus on practical coding scenarios.
`;
const result = await interviewModel.generateContent(prompt);
return result.response.text();
}
export async function evaluateAnswer(
question: string,
answer: string,
role: string
): Promise<{ score: number; feedback: string; correctAnswer: string }> {
const evaluationPrompt = `
Question: ${question}
Candidate's Answer: ${answer}
Role: ${role}
Evaluate the answer on:
- Technical correctness (0-5)
- Communication clarity (0-3)
- Depth of explanation (0-2)
Provide:
1. Total score (0-10)
2. Constructive feedback
3. A model answer for comparison
Format as JSON.
`;
const result = await interviewModel.generateContent(evaluationPrompt);
const text = result.response.text();
return JSON.parse(text);
}
Step 4: Interview API Route
Create app/api/interview/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { generateQuestion, evaluateAnswer } from '@/lib/gemini';
import { prisma } from '@/lib/prisma';
export async function POST(req: NextRequest) {
const { action, data } = await req.json();
switch (action) {
case 'start': {
const { role, difficulty, userId } = data;
const firstQuestion = await generateQuestion(role, difficulty, []);
const interview = await prisma.interview.create({
data: {
userId,
role,
difficulty,
questions: [firstQuestion],
answers: [],
scores: [],
},
});
return NextResponse.json({ interviewId: interview.id, question: firstQuestion });
}
case 'answer': {
const { interviewId, answer, questionIndex } = data;
const interview = await prisma.interview.findUnique({
where: { id: interviewId },
});
if (!interview) {
return NextResponse.json({ error: 'Interview not found' }, { status: 404 });
}
const currentQuestion = interview.questions[questionIndex] as string;
const evaluation = await evaluateAnswer(currentQuestion, answer, interview.role);
// Update interview with answer and score
const updatedAnswers = [...(interview.answers as string[]), answer];
const updatedScores = [...(interview.scores as number[]), evaluation.score];
// Generate next question if less than 5
let nextQuestion = null;
if (questionIndex + 1 < 5) {
const context = updatedAnswers.map((a, i) =>
`Q: ${interview.questions[i]}\nA: ${a}`
);
nextQuestion = await generateQuestion(interview.role, interview.difficulty, context);
await prisma.interview.update({
where: { id: interviewId },
data: {
answers: updatedAnswers,
scores: updatedScores,
questions: [...(interview.questions as string[]), nextQuestion],
},
});
} else {
// Interview complete
const finalScore = updatedScores.reduce((a, b) => a + b, 0) / updatedScores.length;
await prisma.interview.update({
where: { id: interviewId },
data: {
answers: updatedAnswers,
scores: updatedScores,
finalScore,
completedAt: new Date(),
},
});
}
return NextResponse.json({
evaluation,
nextQuestion,
isComplete: questionIndex + 1 >= 5,
finalScore: updatedScores.reduce((a, b) => a + b, 0) / updatedScores.length,
});
}
default:
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
}
}
Step 5: Interview UI Component
Create app/components/InterviewSession.tsx:
'use client';
import { useState, useRef } from 'react';
import { useSpeechRecognition } from 'react-speech-kit';
export default function InterviewSession({ role, difficulty, userId }: Props) {
const [currentQuestion, setCurrentQuestion] = useState('');
const [answer, setAnswer] = useState('');
const [feedback, setFeedback] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [questionNumber, setQuestionNumber] = useState(1);
const [interviewId, setInterviewId] = useState<string | null>(null);
const { listen, listening, stop } = useSpeechRecognition({
onResult: (result: string) => setAnswer(result),
});
const startInterview = async () => {
setIsLoading(true);
const res = await fetch('/api/interview', {
method: 'POST',
body: JSON.stringify({ action: 'start', data: { role, difficulty, userId } }),
});
const data = await res.json();
setInterviewId(data.interviewId);
setCurrentQuestion(data.question);
setIsLoading(false);
};
const submitAnswer = async () => {
setIsLoading(true);
const res = await fetch('/api/interview', {
method: 'POST',
body: JSON.stringify({
action: 'answer',
data: { interviewId, answer, questionIndex: questionNumber - 1 },
}),
});
const data = await res.json();
setFeedback(data.evaluation);
if (data.isComplete) {
alert(`Interview complete! Score: ${data.finalScore}/10`);
} else {
setTimeout(() => {
setCurrentQuestion(data.nextQuestion);
setAnswer('');
setFeedback(null);
setQuestionNumber(prev => prev + 1);
}, 3000);
}
setIsLoading(false);
};
return (
<div className="max-w-2xl mx-auto p-6">
{!interviewId ? (
<button onClick={startInterview} className="btn-primary">
Start Interview
</button>
) : (
<>
<div className="mb-6">
<h2 className="text-xl font-bold">Question {questionNumber}/5</h2>
<p className="mt-2 text-gray-700">{currentQuestion}</p>
</div>
<div className="mb-4">
<textarea
value={answer}
onChange={(e) => setAnswer(e.target.value)}
className="w-full p-2 border rounded"
rows={4}
placeholder="Type your answer..."
/>
<button
onClick={listen}
className={`mt-2 mr-2 ${listening ? 'bg-red-500' : 'bg-blue-500'} text-white p-2 rounded`}
>
{listening ? 'Recording...' : '🎤 Voice Input'}
</button>
{listening && <button onClick={stop} className="ml-2">Stop</button>}
</div>
<button
onClick={submitAnswer}
disabled={isLoading || !answer.trim()}
className="btn-primary"
>
{isLoading ? 'Evaluating...' : 'Submit Answer'}
</button>
{feedback && (
<div className="mt-6 p-4 bg-gray-100 rounded">
<h3 className="font-bold">Feedback</h3>
<p>Score: {feedback.score}/10</p>
<p>{feedback.feedback}</p>
<details className="mt-2">
<summary>Model Answer</summary>
<p className="mt-2">{feedback.correctAnswer}</p>
</details>
</div>
)}
</>
)}
</div>
);
}
Step 6: Code Evaluation Challenge
For technical interviews, add a code editor using Monaco:
npm install @monaco-editor/react
import Editor from '@monaco-editor/react';
// In your component:
<Editor
height="400px"
defaultLanguage="typescript"
value={code}
onChange={(value) => setCode(value || '')}
theme="vs-dark"
/>
Then evaluate code:
async function evaluateCode(question: string, code: string) {
const prompt = `
Question: ${question}
Candidate's Code: ${code}
Evaluate for:
- Correctness (does it solve the problem?)
- Time/Space complexity
- Code style and best practices
- Edge case handling
Return JSON with score (0-10) and detailed feedback.
`;
// ... call Gemini
}
Deployment & Scaling
- Database: Use Neon PostgreSQL for serverless
- API rate limits: Implement Upstash Redis for user quotas
- Voice processing: Offload to Web Speech API (client-side)
- Caching: Cache common questions and evaluations
Conclusion
You’ve built a sophisticated AI interview platform that:
- Generates dynamic questions based on role and difficulty
- Evaluates answers with detailed scoring
- Supports voice input for realistic practice
- Tracks progress across sessions
Extend it further:
- Add system design whiteboard (Excalidraw integration)
- Generate personalized study plans based on weak areas
- Record sessions for self-review
- Multiplayer mock interviews (two candidates practicing together)
Check the GitHub repo for the complete source code and a live demo.
Comments
Join the conversation — sign in to leave a comment.