AI-generated practice tests with a topic-level breakdown of your results.
Pick a subject (or type a specific topic), sit a timed multiple-choice paper, and get back an analysis that says which topics cost you marks — not just a score.
Built as a single Next.js app: the UI, the API and the database access all live here.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, React 19, Turbopack) |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS v4, CSS-variable design tokens, light + dark themes via next-themes |
| Database | MongoDB + Mongoose 9 |
| Auth | JWT in an httpOnly cookie, signed with jose; passwords hashed with bcryptjs |
| AI | Google Gemini (gemini-3.6-flash) with a JSON response schema |
| Charts | Recharts for the accuracy trend; plain HTML bars for topic breakdowns |
| Validation | Zod on every API route |
- Node.js 20.9 or newer
- A MongoDB database — Atlas free tier or a local
mongod - A Google AI Studio API key — https://aistudio.google.com/apikey
npm installcp .env.example .env.localThen fill in .env.local — see Environment variables below.
npm run devOpen http://localhost:3000, create an account, and generate your first test.
All of these go in .env.local (git-ignored). .env.example is the template.
| Variable | Required | What it is |
|---|---|---|
MONGODB_URI |
Yes | MongoDB connection string. Atlas: mongodb+srv://<user>:<pass>@<cluster>.mongodb.net/topiciq?retryWrites=true&w=majority. Local: mongodb://127.0.0.1:27017/topiciq |
JWT_SECRET |
Yes | Secret that signs the session cookie. At least 32 characters. Changing it signs everyone out. |
GEMINI_API_KEY |
Yes | Google AI Studio key used to generate questions. |
GEMINI_MODEL |
No | Overrides the model. Defaults to gemini-3.6-flash. |
Generate a JWT_SECRET with:
node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"No NEXT_PUBLIC_* variables are needed — the browser never talks to Gemini or MongoDB
directly, only to this app's own API routes.
/start— you choose level, subject, optional topic, difficulty, length and duration.POST /api/tests— the server asks Gemini for questions using a strict JSON response schema, shuffles each question's options, and saves the paper as aTestdocument with statusin_progress./test/[testId]— the runner loads the questions without their correct answers. It tracks per-question time, supports flagging and keyboard entry, and saves progress tolocalStorageso a refresh does not lose the attempt.POST /api/tests/[testId]/submit— the server grades the attempt against the stored questions, computes accuracy and marks the testcompleted. Submitting twice is a no-op./analysis/[testId]— accuracy, pace, per-topic performance and a full worked review of every question, filterable by correct / incorrect / skipped./dashboard— accuracy over time, topic mastery across all papers, strongest and weakest topics, and your test history.
Correct answers only ever reach the browser once a test is submitted, and grading happens on the server — the score cannot be edited from the client.
src/
app/
layout.tsx root layout, fonts, theme provider
page.tsx landing page
auth/ sign in / sign up
(app)/ signed-in shell (header, footer, auth guard)
dashboard/
start/
analysis/[testId]/
test/[testId]/ distraction-free test runner
api/
auth/{signup,login,logout}/
tests/ POST: generate + create a paper
tests/[testId]/submit/ POST: grade and complete
components/ UI, charts, runner, review
lib/
mongodb.ts cached connection
models/ Mongoose schemas
session.ts / auth.ts JWT cookie + server guards
gemini.ts question generation
queries.ts server-side reads for pages
proxy.ts route protection (Next 16 proxy convention)
npm run dev # development server
npm run build # production build
npm run start # serve the production build
npm run lint # eslintWorks on any Node host. On Vercel, add MONGODB_URI, JWT_SECRET and GEMINI_API_KEY as
project environment variables, and allow your deployment's IPs in MongoDB Atlas network
access (0.0.0.0/0 for a quick start).
Question generation for 30 questions can take longer than a free-tier function timeout.
The route sets maxDuration = 120; make sure your plan allows it, or keep tests to 10–20
questions.