Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nayalsaurav/resume-analyzer/llms.txt

Use this file to discover all available pages before exploring further.

Resume Check Karo uses Google Gemini as its sole AI engine, accessed through the official @google/genai SDK. Every resume analysis is a single multimodal generateContent call: the PDF is uploaded inline as base64-encoded binary data alongside a structured text prompt, and the model is constrained to return a fully typed JSON response matching a predefined schema. No fine-tuning or embeddings are involved — the entire intelligence lives in the prompt and schema.

Client Initialisation

The Gemini client is a module-level singleton created once in lib/google.ts. The API key is read from the server environment so it is never bundled into client-side JavaScript.
import { GoogleGenAI, Type } from "@google/genai";

const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY });
The model name is also environment-controlled:
model: process.env.GOOGLE_AI_MODEL!
Use gemini-1.5-flash in GOOGLE_AI_MODEL for faster and cheaper analysis — ideal for development and high-traffic production workloads. Switch to gemini-1.5-pro when you need more thorough, nuanced feedback on complex resumes. No code changes are required; only the environment variable needs to change.

Sending PDFs to the Model

Gemini’s multimodal API accepts file data as base64-encoded strings using the inlineData part type. getAiResponse() reads the uploaded File object into an ArrayBuffer, converts it to a Node.js Buffer, then base64-encodes it before attaching it to the contents array alongside the text prompt.
export async function getAiResponse(file: File, instruction: string) {
  let jsonOutput = "";
  try {
    const buffer = await file.arrayBuffer();
    const fileBuffer = Buffer.from(buffer);

    const response = await ai.models.generateContent({
      model: process.env.GOOGLE_AI_MODEL!,
      contents: [
        {
          role: "user",
          parts: [
            { text: instruction },
            {
              inlineData: {
                data: fileBuffer.toString("base64"),
                mimeType: "application/pdf",
              },
            },
          ],
        },
      ],
      config: {
        responseMimeType: "application/json",
        responseSchema: resumeAnalysisSchema,
      },
    });

    jsonOutput = response?.text ?? "";

    if (!jsonOutput) {
      throw new Error("Empty response from AI model");
    }

    return JSON.parse(jsonOutput) as Feedback;
  } catch (err) {
    console.error("Failed to get AI response:", {
      error: err instanceof Error ? err.message : err,
      jsonOutput,
      fileName: file.name,
      fileSize: file.size,
    });

    return createErrorFeedback();
  }
}
The inlineData approach is suitable for PDFs up to ~20 MB. For larger files the Gemini Files API (ai.files.upload()) is the recommended alternative — it uploads the file to Google’s servers first and passes a file URI instead of base64 data.

Structured JSON Output

Setting responseMimeType: "application/json" and providing a responseSchema puts the model into constrained decoding mode. Rather than generating free-form text that happens to include JSON, the model’s token sampling is restricted to sequences that conform to the schema at every step. This means:
  • The response is always valid JSON — no markdown fences, no preamble text, no apologies.
  • Every required field in the schema (overallScore, ATS, toneAndStyle, content, structure, skills) is guaranteed to be present.
  • Enum constraints on FeedbackTip.type ("good" | "improve") are enforced by the model itself.
The parsed JSON is cast directly to the Feedback TypeScript interface with no additional validation layer needed.

The resumeAnalysisSchema

The schema is expressed using the SDK’s Type enum constants, which map to the underlying JSON Schema types accepted by the Gemini API.
import { Type } from "@google/genai";

export const resumeAnalysisSchema = {
  type: Type.OBJECT,
  properties: {
    overallScore: { type: Type.NUMBER },
    ATS: {
      type: Type.OBJECT,
      properties: {
        score: { type: Type.NUMBER },
        tips: {
          type: Type.ARRAY,
          items: {
            type: Type.OBJECT,
            properties: {
              type: { type: Type.STRING, enum: ["good", "improve"] },
              tip: { type: Type.STRING },
            },
            required: ["type", "tip"],
          },
        },
      },
      required: ["score", "tips"],
    },
    toneAndStyle: {
      type: Type.OBJECT,
      properties: {
        score: { type: Type.NUMBER },
        tips: {
          type: Type.ARRAY,
          items: {
            type: Type.OBJECT,
            properties: {
              type: { type: Type.STRING, enum: ["good", "improve"] },
              tip: { type: Type.STRING },
              explanation: { type: Type.STRING },
            },
            required: ["type", "tip", "explanation"],
          },
        },
      },
      required: ["score", "tips"],
    },
    // content, structure, and skills follow the same shape as toneAndStyle
  },
  required: ["overallScore", "ATS", "toneAndStyle", "content", "structure", "skills"],
};
ATS tips do not include an explanation field — they are intentionally short, scannable bullet points. The toneAndStyle, content, structure, and skills sections require "type", "tip", and "explanation" as part of their schema definition.

Prompt Construction

The text instruction passed to getAiResponse() is assembled by prepareInstructions() in lib/constants.ts. The function injects the user-provided context and evaluation criteria into a structured prompt template. The prompt instructs the model to:
  1. Evaluate against the specific role — Company name, job title, and the full job description are injected so the analysis is tailored to the position, not generic.
  2. Score each category 0–100, and be critical — The prompt explicitly instructs the model to avoid inflated scores. A resume that merely has the correct sections should score in the 50–65 range; only exceptional resumes should approach 90+.
  3. Return at least one tip per category — Every tips array must contain at least one actionable item, even if the section is strong.
  4. Distinguish “good” from “improve” tips — Tips labelled "good" acknowledge strengths; tips labelled "improve" call out specific, fixable issues.
A simplified illustration of the injected values:
function prepareInstructions({
  companyName,
  jobTitle,
  jobDescription,
}: {
  companyName: string;
  jobTitle: string;
  jobDescription: string;
}): string {
  return `
You are an expert resume reviewer and career coach.

Analyse the attached PDF resume for a candidate applying to ${companyName}
for the role of ${jobTitle}.

Job Description:
${jobDescription}

Evaluate the resume across five dimensions and return a JSON object matching
the provided schema exactly:
- ATS Compatibility (keyword match, formatting, parsability)
- Tone & Style (voice, professionalism, consistency)
- Content Quality (impact statements, quantified achievements, relevance)
- Structure & Formatting (layout, section order, readability)
- Skills Relevance (match between listed skills and job requirements)

Score each section 0–100. Be critical — reserve scores above 85 for
genuinely outstanding resumes. Overall score is a weighted average.
  `.trim();
}

Feedback Types

The TypeScript interfaces in lib/google.ts are the single source of truth for the AI response shape. The same types are used in the resumeAnalysisSchema, the server action return value, the Prisma JSON column, and the React components that render feedback.
export interface FeedbackTip {
  type: "good" | "improve";
  tip: string;
  explanation?: string; // optional; present on toneAndStyle, content, structure, skills
}

export interface FeedbackSection {
  score: number; // 0–100
  tips: FeedbackTip[];
}

export interface Feedback {
  overallScore: number;    // weighted aggregate of all five section scores
  ATS: FeedbackSection;
  toneAndStyle: FeedbackSection;
  content: FeedbackSection;
  skills: FeedbackSection;
  structure: FeedbackSection;
}

Error Handling

If the Gemini API call fails for any reason — network error, rate limit, or empty response — getAiResponse() catches the error and calls createErrorFeedback(). This returns a Feedback object with all scores set to 0 and an "improve" tip in each section indicating that analysis could not be completed.
function createErrorFeedback(): Feedback {
  return {
    overallScore: 0,
    ATS: {
      score: 0,
      tips: [{ type: "improve", tip: "Analysis failed: internal server error" }],
    },
    toneAndStyle: {
      score: 0,
      tips: [{ type: "improve", tip: "Unable to analyze" }],
    },
    content: {
      score: 0,
      tips: [{ type: "improve", tip: "Unable to analyze" }],
    },
    structure: {
      score: 0,
      tips: [{ type: "improve", tip: "Unable to analyze" }],
    },
    skills: {
      score: 0,
      tips: [{ type: "improve", tip: "Unable to analyze" }],
    },
  };
}
This ensures the Resume record is still created in the database (with analysis set to the error feedback object) and the user sees a meaningful message rather than an unhandled exception page.
Gemini API rate limits vary by model and billing tier. If you encounter 429 Too Many Requests errors in production, implement exponential back-off retry logic around the ai.models.generateContent() call or upgrade to a paid quota tier in Google AI Studio.

Environment Variables

The AI integration requires two environment variables. Neither is ever exposed to the browser.
VariableDescription
GOOGLE_API_KEYYour Google AI Studio API key. Found at aistudio.google.com.
GOOGLE_AI_MODELThe Gemini model identifier, e.g. gemini-1.5-flash or gemini-1.5-pro.
# .env.local
GOOGLE_API_KEY=AIza...
GOOGLE_AI_MODEL=gemini-1.5-flash
Swapping GOOGLE_AI_MODEL from gemini-1.5-flash to gemini-1.5-pro requires only an environment variable change — no code modifications. Flash is recommended as the default for its lower latency and cost; Pro is available when analysis depth matters more than speed.

Build docs developers (and LLMs) love