ExamNotesAI is a full-stack AI-powered w...

by Unattributed

Audio version created with Paper2Audio.

Listen on Paper2Audio

Audio by Paper2Audio

ExamNotesAI is a full-stack A.I-powered web application that I built to help students generate structured, exam-oriented study notes.

The main problem I wanted to solve was that students often have a large amount of study material but limited time before exams. Existing A.I tools can generate information, but the output is often just unstructured text. I wanted to build something specifically focused on exam preparation where the generated information could be organized into structured notes and later rendered properly in the application or exported as a P.D.F.

For the technology stack, I used React with Vite for the frontend, Redux Toolkit for application-level state management, Node.js and Express.js for the backend, and MongoDB with Mongoose for the database. I integrated the Gemini A.P.I for A.I-based content generation. I also implemented authentication, a credit-based usage system, Stripe payment integration, and P.D.F generation.

Let me explain the architecture.

At the frontend, the user interacts with the React application. When the user wants to generate notes, the frontend collects information such as the topic and other exam-related parameters and sends an H.T.T.P request to my Express backend.

The request first passes through the backend authentication layer. I need to verify the identity of the user because note generation is a protected operation and is also associated with the user's credit balance.

Once the user is authenticated, the controller validates the request and checks whether the user has sufficient credits.

Then comes the A.I-generation part.

Instead of calling Gemini directly from React, I call Gemini from my backend. This is an important design decision because if I called Gemini directly from the browser, my Gemini A.P.I key could potentially be exposed to the client.

The backend creates a structured prompt using the user's input and sends that prompt to Gemini.

One of the most interesting technical challenges I faced was that I didn't want Gemini to return just a large block of text. I wanted structured information that my application could understand.

So I designed the prompt to request a specific jay-sun structure.

The problem is that an L.L.M isn't a deterministic function. Even if you ask it to return jay-sun, it may sometimes wrap the jay-sun inside Markdown code fences or return something that isn't perfectly valid jay-sun.

I handled this in two ways.

First, I made my prompt much more restrictive. I specified the expected fields and formatting rules.

Second, I created backend processing logic that cleans the Gemini response before calling jay-sun.parse(). For example, if Gemini returns Markdown jay-sun fences, I remove those first.

The parsing itself is wrapped in error handling. If Gemini returns malformed output, I don't blindly save that response into MongoDB. Instead, the request fails cleanly.

Once the generated content is successfully parsed, I create a Notes document in MongoDB.

The application primarily has a user model and a notes model. The user stores information such as credits and references to generated notes. The Notes collection stores information about each generated note, including the topic, exam-related metadata and the A.I-generated content.

For the generated content, I needed flexibility because every note may not have exactly the same structure. Some generated responses may contain different sections or additional structured content. That's one reason MongoDB was a good fit for this project.

After successfully generating the note, I associate the note with the user and update the credit information.

The frontend then receives the structured response and renders it.

I also implemented a P.D.F-generation feature using P.D.F Kit. The backend can take the structured notes and generate a downloadable P.D.F.

Another feature I implemented was a credit-purchasing system using Stripe.

The frontend doesn't directly decide whether payment was successful. Instead, the backend creates a Stripe Checkout session. After the user completes payment, Stripe sends a webhook to my backend. The backend verifies the Stripe webhook signature before trusting the event and updating the user's credits.

This was important because trusting a success message coming only from the frontend would be insecure. Someone could potentially manipulate the frontend or directly call the A.P.I.

For authentication, I used Firebase for the Google authentication flow along with J.W.T-based application authentication. The J.W.T is stored using an H.T.T.P-only cookie so that frontend JavaScript cannot directly access it, which helps reduce token theft through X.S.S.

Overall, ExamNotesAI taught me much more than simply integrating an A.I A.P.I. It gave me experience with complete application architecture, rest A.P.I's, authentication, MongoDB data modeling, external A.P.I integration, payments, error handling, application security and deployment.

The biggest lesson was that external services should always be treated as potentially unreliable. Gemini can return malformed data, network calls can fail, Stripe events need verification, and database operations can fail. So a robust application needs to think about the failure path as much as the successful path.”

Your project uses UserModel and Notes, with generated content stored flexibly, and includes Firebase/J.W.T authentication, Gemini integration, Stripe, and P.D.F generation.

If they ask, “What would you improve?”

Say:

“The current project works well as a student project, but I wouldn't call the architecture production-ready at very large scale.

If I had to improve it, the first thing I would change would be A.I generation. Gemini can be slow, so instead of keeping an H.T.T.P request waiting, I would introduce an asynchronous job queue. The A.P.I could create a generation job and return a job I.D. Separate workers would process Gemini requests.

I would also add caching, stronger rate limiting, better database indexing based on actual query patterns, structured logging and monitoring.

For payment processing, I would make webhook handling fully idempotent so that if Stripe sends the same event multiple times, credits are added only once.

I would also make logically related database operations transactional where necessary. For example, creating a note, associating it with the user and deducting credits should ideally behave as one logical operation.”