AI Interviews
May 3, 2026•8 min read•...
AI InterviewsMay 3, 2026•8 min read•

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

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

bash
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:

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:

typescript
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:

typescript
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:

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:

bash
npm install @monaco-editor/react
tsx
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:

typescript
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.