ProjectWeb Development · 2024
QuizItNow
Turn any content into quizzes with AI. Generate assessments from text, URLs, PDFs and YouTube videos, with difficulty levels you control.
- Next.js
- TypeScript
- React
- MongoDB
- NVIDIA AI
- shadcn/ui
- Tailwind CSS
- NextAuth.js
- AWS S3
- Vercel AI SDK
- Zod

Web Development · Project
QuizItNow turns a piece of content into a quiz. Paste text, drop in a YouTube link, point it at an article, or upload a PDF. It returns questions at the difficulty you asked for. I built it because writing good questions by hand is slow, and most of that slowness is mechanical rather than pedagogical.
The whole system is one Next.js application. Extraction, generation and storage all happen inside API routes, so there is no second service to deploy or keep in step. That was not the original shape.
# input: a YouTube link, difficulty God ModePOST /api/quiz/generate/ai inputType=linktranscript extracted, 1842 words (min 250)generateObject -> generatedQuizSchema validated200 quizId=8f31c0 10 questions, God ModeGET /api/quiz/8f31c0 4 options per questionGET /api/quiz 1 quiz for this userFrom Python to TypeScript
It started as a FastAPI backend with a separate Next.js frontend. Two languages meant two type systems, two deploy targets and a constant context switch. I rewrote the backend in TypeScript inside the Next.js app, and the routes and the interface began sharing types. In the same pass I moved from LangChain to the Vercel AI SDK, from PostgreSQL to MongoDB, and rebuilt authentication on NextAuth.js v5 with Google and GitHub. The cost is that generation now runs inside a request handler. A slow PDF download and a 70B model call share one function timeout, which a separate worker would not.
Getting the content in
app/api/quiz/generate/ai/route.ts decides how to read the input before it prompts anything. A Zod schema validates the body, and a superRefine rule rejects a file input sent as a bare string. YouTube links go to youtube-transcript. Links ending in .pdf are downloaded and handed to pdf-parse. Everything else is fetched and run through Cheerio with script, style and noscript stripped out. Uploads reach S3 through a presigned URL from POST /api/s3 and are read back from there. Whichever path ran, the extracted text has to clear 250 words before a prompt is built.
Constraining the model
Language models are happy to return prose where I need an object. generateObject with a Zod schema pins the output to the shape MongoDB expects, at a temperature of 0.3.
export const generatedQuizSchema = z.object({
title: z.string().min(1),
description: z.string().min(1),
difficulty: z.enum(['Easy', 'Medium', 'Hard', 'God Mode']),
topic: z.string().min(1),
tags: z.array(z.string().min(1)).default([]),
questions: z.array(generatedQuestionSchema).min(1),
});
A valid object is still not a correct one. sanitizeGeneratedQuiz collapses true or false options back to True and False, and pushes the stated answer into the option list when the model leaves it out. A wrong question count is treated as a failed call, not as output.
Decisions and their costs
- Requests walk a list of models instead of one. The chosen model gets two attempts, then Llama 3.1 8B, Llama 3.3 70B, Mistral 7B, Gemma 2 9B, Qwen 2.5 7B and Nemotron Mini take their turn. Something almost always comes back. A user can also quietly get a smaller model, and only the stored
aiModelfield records it. - Anonymous generation works and is never saved.
userIddefaults toanonymous, and the database write is skipped for that value. Trying the product costs no signup, and the quiz dies with the tab. - MongoDB replaced PostgreSQL because a quiz is one nested document with a variable question shape. Reading one is a single lookup on
quizId. Anything analytical across questions becomes application code. - Difficulty changes the prompt, never the sampling. I would revisit this first. The four tiers are prose instructions inside one long template, and I have no measurement that tells them apart.
Generating a quiz
One call covers the whole path from a video to stored questions.
curl -X POST "$APP_URL/api/quiz/generate/ai" \
-H 'Content-Type: application/json' \
-d '{"input":"https://youtu.be/VIDEO_ID","inputType":"link",
"numberOfQuestions":10,"difficulty":"God Mode"}'
The response carries a quizId, the sanitized quiz and the model that produced it. GET /api/quiz/[quizId] reads it back and GET /api/quiz lists what the signed-in user has saved. Running it locally needs MONGODB_URI, NVIDIA_API_KEY and AUTH_SECRET. The AWS variables only matter for uploads, and NVIDIA_BASE_URL already points at the NIM endpoint.
Where it stops
Generation is synchronous, so a slow extraction and a slow model add up to a long spinner and nothing else. POST /api/audio/transcribe is a placeholder that transcribes nothing. The prompt templates still instruct the model to refuse anything under 500 words while the route enforces 250, which is a leftover from moving that floor. There is no rate limiting on the generation route. It runs as a public demo, builds into a Docker image serving port 3000 for self-hosting, and is MIT licensed.
Next projectDesign Engineering