Skip to main content

API structure

All API routes are located in app/api/ using Next.js 16 Route Handlers:
app/api/
├── parse-resume/
│   └── route.ts          # PDF parsing
├── extract-info/
│   └── route.ts          # AI resume extraction
├── user/
│   ├── route.ts          # Get user data
│   ├── update/
│   │   └── route.ts      # Update user
│   └── publish-resume/
│       └── route.ts      # Publish website
└── user-image/
    └── route.ts          # Upload profile image

Parse resume endpoint

Parses PDF files to extract text content.

Endpoint

POST /api/parse-resume

Implementation

export const runtime = "nodejs";

import { NextRequest, NextResponse } from "next/server";

let SmartPDFParser: any;
let parserInstance: any;

async function getParser() {
  if (!parserInstance) {
    const mod = await import("pdf-parse-new");
    SmartPDFParser = mod.SmartPDFParser;
    parserInstance = new SmartPDFParser({
      oversaturationFactor: 1.5,
      enableFastPath: true,
      forceMethod: "batch",
    });
  }
  return parserInstance;
}

export async function POST(req: NextRequest) {
  try {
    const formData = await req.formData();
    const file = formData.get("file") as File | null;

    if (!file || file.type !== "application/pdf") {
      return NextResponse.json(
        { msg: "Please upload a valid PDF file" },
        { status: 400 }
      );
    }

    // 5MB size limit
    if (file.size > 5 * 1024 * 1024) {
      return NextResponse.json(
        { msg: "File too large (Max 5MB)" },
        { status: 413 }
      );
    }

    const parser = await getParser();
    const buffer = Buffer.from(await file.arrayBuffer());
    const data = await parser.parse(buffer);

    return NextResponse.json(data);
  } catch (error) {
    console.error("PDF Parsing Error:", error);
    return NextResponse.json(
      { msg: "Failed to parse PDF", details: (error as Error).message },
      { status: 500 }
    );
  }
}

Request

const formData = new FormData();
formData.append("file", pdfFile);

const response = await fetch("/api/parse-resume", {
  method: "POST",
  body: formData,
});

const { text, numpages } = await response.json();

Response

{
  "text": "John Doe\nSoftware Engineer\n...",
  "numpages": 2,
  "info": {
    "Title": "Resume",
    "Author": "John Doe"
  }
}
The parser is configured with forceMethod: "batch" for serverless compatibility. Files are limited to 5MB to prevent timeouts.

Extract info endpoint

Uses OpenRouter AI to structure resume text into JSON.

Endpoint

POST /api/extract-info

Implementation

import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";

const openai = new OpenAI({
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENROUTER_API_KEY,
});

export async function POST(req: NextRequest) {
  try {
    const { text } = await req.json();

    const prompt = `
Convert the following resume text into valid structured JSON.

Return ONLY JSON (no markdown, no explanation).

Schema:
{
  "personalInfo": {
    "name": "string",
    "title": "string",
    "email": "string",
    "phone": "string",
    "location": "string",
    "website": "string",
    "linkedin": "string",
    "github": "string"
  },
  "summary": "string",
  "skills": {
    "languages": ["string"],
    "frameworksAndTools": ["string"],
    "softSkills": ["string"]
  },
  "experience": [{
    "company": "string",
    "position": "string",
    "location": "string",
    "startDate": "string",
    "endDate": "string",
    "isCurrentRole": "boolean",
    "description": ["string"],
    "technologies": ["string"]
  }],
  "projects": [{
    "name": "string",
    "role": "string",
    "startDate": "string",
    "endDate": "string",
    "link": "string | null",
    "description": ["string"],
    "technologies": ["string"]
  }],
  "education": [{
    "university": "string",
    "degree": "string",
    "branch": "string",
    "location": "string",
    "sgpa": "string",
    "startDate": "string",
    "endDate": "string"
  }]
}

Resume Text:
${text}
`;

    const completion = await openai.chat.completions.create({
      model: process.env.OPENROUTER_MODEL_NAME!,
      messages: [
        {
          role: "system",
          content: "You are a resume parser that outputs only valid JSON.",
        },
        { role: "user", content: prompt },
      ],
      temperature: 0,
      response_format: { type: "json_object" },
    });

    const raw = completion.choices[0].message.content;
    const parsedJSON = JSON.parse(raw!);

    return NextResponse.json(parsedJSON);
  } catch (error) {
    return NextResponse.json(
      { msg: "Failed to parse resume", details: (error as Error).message },
      { status: 500 }
    );
  }
}

Request

const response = await fetch("/api/extract-info", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ text: resumeText }),
});

const resume = await response.json();

Response

{
  "personalInfo": {
    "name": "John Doe",
    "title": "Software Engineer",
    "email": "[email protected]",
    "phone": "+1-555-0123",
    "location": "San Francisco, CA",
    "website": "johndoe.com",
    "linkedin": "linkedin.com/in/johndoe",
    "github": "github.com/johndoe"
  },
  "summary": "Experienced software engineer...",
  "skills": {
    "languages": ["JavaScript", "TypeScript", "Python"],
    "frameworksAndTools": ["React", "Next.js", "Node.js"],
    "softSkills": ["Team Leadership", "Problem Solving"]
  },
  "experience": [
    {
      "company": "Tech Corp",
      "position": "Senior Engineer",
      "location": "San Francisco, CA",
      "startDate": "Jan 2020",
      "endDate": "Present",
      "isCurrentRole": true,
      "description": ["Led team of 5 engineers", "Built scalable systems"],
      "technologies": ["React", "Node.js", "AWS"]
    }
  ],
  "projects": [],
  "education": []
}
This endpoint uses OpenRouter AI credits. Monitor your usage to avoid unexpected costs.

User endpoints

Get user data

Retrieves user information from Supabase. Endpoint: GET /api/user
import { getUserData, UserField } from "@/lib/supabase/user/getUserData";
import { NextRequest, NextResponse } from "next/server";

export async function GET(req: NextRequest) {
  const { searchParams } = req.nextUrl;
  const fieldsParam = searchParams.get("fields");

  let fields: UserField[] | undefined;

  if (fieldsParam) {
    const allowedFields: UserField[] = [
      "id", "email", "username", "resume", "created_at", "islive"
    ];
    fields = fieldsParam
      .split(",")
      .filter((f) => allowedFields.includes(f as UserField)) as UserField[];
  }

  const data = await getUserData(fields);

  if (!data) {
    return NextResponse.json(
      { msg: "User not found or unauthorized" },
      { status: 404 }
    );
  }

  return NextResponse.json(data);
}
Request:
const response = await fetch("/api/user");
const user = await response.json();
Response:
{
  "id": "uuid",
  "email": "[email protected]",
  "username": "johndoe",
  "resume": { /* Resume object */ },
  "islive": true,
  "created_at": "2024-01-15T12:00:00Z"
}

Update user

Endpoint: PATCH /api/user/update
export async function PATCH(req: NextRequest) {
  const body = await req.json();
  const updated = await updateUserData(body);
  return NextResponse.json(updated);
}
Request:
await fetch("/api/user/update", {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    username: "newusername",
    resume: resumeData,
  }),
});

Publish resume

Endpoint: POST /api/user/publish-resume Makes user’s website publicly accessible.
export async function POST(req: NextRequest) {
  const { islive } = await req.json();
  await publishResume(islive);
  return NextResponse.json({ success: true });
}
Request:
await fetch("/api/user/publish-resume", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ islive: true }),
});

User image

Endpoint: GET /api/user-image Retrieves a user’s profile image URL from Clerk.
app/api/user-image/route.ts
export async function GET(req: NextRequest) {
  const userId = req.nextUrl.searchParams.get("userId");

  if (!userId) {
    return NextResponse.json({ msg: "Missing userId" }, { status: 400 });
  }

  try {
    const client = await clerkClient();
    const user = await client.users.getUser(userId);

    return NextResponse.json({
      imageUrl: user.hasImage ? user.imageUrl : null,
    });
  } catch {
    return NextResponse.json({ msg: "User not found" }, { status: 404 });
  }
}

Error handling

All API routes follow consistent error handling:
try {
  // Operation
  return NextResponse.json(data);
} catch (error) {
  console.error("Error:", error);
  return NextResponse.json(
    { msg: "Error message", details: (error as Error).message },
    { status: 500 }
  );
}

Authentication

API routes use Clerk for authentication via server-side helpers:
import { currentUser } from "@clerk/nextjs/server";

export async function GET() {
  const user = await currentUser();
  
  if (!user) {
    return NextResponse.json(
      { msg: "Unauthorized" },
      { status: 401 }
    );
  }
  
  // Proceed with authenticated user
}

Rate limiting

Consider implementing rate limiting for production:
import { Ratelimit } from "@upstash/ratelimit";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "10 s"),
});

export async function POST(req: NextRequest) {
  const ip = req.ip ?? "127.0.0.1";
  const { success } = await ratelimit.limit(ip);
  
  if (!success) {
    return NextResponse.json(
      { msg: "Too many requests" },
      { status: 429 }
    );
  }
  
  // Process request
}

Best practices

  1. Use TypeScript - Type all request/response payloads
  2. Validate inputs - Check file types, sizes, and data formats
  3. Handle errors - Return consistent error responses
  4. Log errors - Use console.error for debugging
  5. Set timeouts - Configure for serverless environments
  6. Secure secrets - Use environment variables, never hardcode
  7. Return proper status codes - 200, 400, 401, 404, 500, etc.

Testing API routes

Use tools like Postman, Insomnia, or cURL:
curl -X POST http://localhost:3000/api/parse-resume \
  -F "file=@path/to/resume.pdf"

Build docs developers (and LLMs) love