How I Built LanguageDecks.com: Integrating OpenAI with Next.js and .NET
by: Seth Rhodes
10-17-2025
I. Introduction
This blog post highlights the the complexities and simplicities of creating a GPT-wrapper app, the problems that come with it, how I solved said problems, and what I'm thinking for the future.
What is LanguageDecks?
LanguageDecks.com is a web app that allows users to generate flashcard decks for language learning at specific levels with specific theming. The decks are then exportable to Anki to be used for studying. It provides a consistent, reliable, and quick method for generation.
Why a flashcard generator?
Flashcards are the most reliable and useful tool when learning languages. Anki is the gold standard for studying. My problem was that manually creating 1000+ flashcards takes too much time compared to other methods. I needed an automatic flashcard generator for language learning. There are plenty of flashcard generators, even for Anki flashcards, but most work as "automatic flashcards from your uploaded study materials", and none of them were specific to language learning.
Tech Stack
- Next.js for frontend
- Tailwind CSS for styling
- .NET for backend
- PostgreSQL database
- OpenAI API for generation
- Google SSO
- Stripe payment
II. The Vision & Planning Phase
The Plan: Create a LLM-powered language learning flashcard generation platform.
Core Requirements
- Generate comprehensive flashcards (word, definition, examples, translations)
- Persistent access to previously generated flashcard decks
- Audio pronunciations for language learning
- Export to Anki format (the gold standard)
- Real-time progress tracking
III. Architecture Deep Dive
Why a Monorepo?
This can be personal preference but I found that maintaining two separate repos meant more complicated deployment and version control. This way, the current working version is the same for both the backend and frontend.
The Backend (.NET 9 API)
- Repository/Service Architecture: Repositories work as the Data Access Layer, handling all Entity Framework interactions with PostgreSQL. Services work as the Data Manipulation Layer or logic layer,they handle any necessary changes to the data.
- SignalR for Real-time Updates: Used for streaming live generation updates to the frontend, which was necessary for a good user experience when viewing generation progress.
- Why .NET vs. Node Backend: There are arguments for both. The main reason I used a .NET backend was because I'm comfortable with it. There are some benefits like avoiding potential degrading rendering performance from too much computation in Next.js.
The Frontend (Next.js 15)
- Zustand for State Management: Having used both Zustand and Redux, Zustand feels much more lightweight and easier to use. It's worked well so far for this project.
- NSwag for Type-safe API Client Generation: NSwag generates a TypeScript API client based on the OpenAPI spec generated by the .NET backend. This means no guessing of object structures or having to manually update types when the backend changes.
- Server-Side Rendering Strategy: SSR isn't used as much as it could be in the project. It's mostly used for layout and landing pages and SEO optimization. Most pages and components are CSR because of state management, SignalR connections, and interactive UIs. This means the JavaScript bundle is a bit larger than it needs to be. I plan on improving this in the future.
Communication Layer
- REST API for CRUD Operations: Standard REST API endpoints for creating, reading, updating, and deleting decks and cards.
- SignalR for Real-time Progress: WebSocket connections for generation progress were used instead of polling mainly because I wanted the progress updates to be as accurate as possible, as the updates rely on parsing the streamed response from the OpenAI API.
IV. The AI Integration Journey
OpenAI API Setup
I haven't experimented with different models yet. The app is currently using gpt-4o-mini. The prompt was changed frequently over the course of development. It was changed to emphasize card count and output language. Not emphasizing these things gave room for the model to ballpark some outputs (even if both were given).
Structured Output with JSON Schema
Structured outputs were achieved by supplying the model with a JSON response schema with each request.
Text-to-Speech Integration
- OpenAI TTS API was used for audio streaming and downloading audio files for exporting into Anki packages.
- I originally created a blob file storage service in Azure to hold all audio files, and each audio file was originally generated at the initial generation request. This significantly increased generation time.
- I then changed to using audio streaming, so after generation the user can click the audio player on a card, and that text would then be sent in an audio streaming request to OpenAI TTS API and the audio plays as it's generated. It is also cached to be replayed if clicked again.
- The audio is only downloaded as an MP3 when the user selects to export as an Anki package.
Cost Optimization
There aren't any consistent users as of yet, so generation token costs are minimal. The highest cost at the moment is hosting within Azure. I will come back to update this section if the app gains more traction.
V. The Anki Export Challenge
Exporting to Anki was harder than expected. There is no pre-existing library to create Anki packages in .NET, meaning I had to implement an entirely custom Anki package creation service. There is very limited documentation on the Anki package filetype, so I translated logic from pre-existing Python libraries that do the same thing. (copilot was a huge help here, still took a lot of trial and error)
Real-time Export Progress
Just like with the initial card generation, SignalR was used for progress tracking for a better user experience.
VI. Real-time Updates with SignalR
Why SignalR
SignalR basically allows for a better user experience when waiting for long processes to complete. The alternative would be to use polling, which is less efficient and less real-time. The app also currently uses response streaming from the OpenAI API, so SignalR was a natural fit to stream progress updates to the frontend.
Hub Architecture
- Deck generation hub
- Export progress hub
Frontend Integration
- React hooks for SignalR connections
- State management with Zustand
- Reconnection handling: it was important to handle reconnections in case of network issues. The app uses SignalR's built-in reconnection features to ensure that the connection is re-established if it drops.
VII. Authentication & Payment Flow
Google OAuth Integration
- Why Google: User convenience
- JWT Token Management: All user authorization in the app uses custom JWT supplied by the backend, so any SSO flows still direct to the backend to create a custom token for that user.
Security Considerations
I haven't implemented account creation, only SSO flows. This allows me to avoid storing most account info. Using Stripe for payment flows allows for avoiding handling payment info.
The app uses Azure Key Vault to store all secrets and sensitive info.
VIII. Deployment & Production
Azure Architecture
- App Service for .NET API
- Static Web Apps for Next.js
- PostgreSQL Flexible Server
Secrets Management
- Azure Key Vault integration
- Environment-specific configurations
IX. Challenges & Lessons Learned
When generating more than 200 cards at a time, the total time to generate can go over 5 minutes. I want to keep total generation time under 3 minutes maximum. The solution I chose was to create simultaneous OpenAI calls for 25 cards at a time. This significantly reduced generation time but introduced a new problem: duplicate cards. Each request was sent with the same exact instructions, but with no context of already generated cards.
After playing around with some solutions, I concluded that the simplest, yet effective solution was to limit card generations to 100 cards at a time but introduce continued generation into a deck. This means when a user visits a deck in the app, there is an option to "add cards" which navigates the user through the typical deck creation process again, adding 100 cards at a time to the deck.
This solution removes the need for simultaneous requests, preventing duplicate generations within the same response. It also allows the user to have more control over the deck's theming and contents to a very specific card count, increased card counts to large amounts (1000+) while still limiting the waiting time to 100 cards all while avoiding the introduction of complicated solutions that could add more maintenance for the app.
IX. Future Improvements
The problem of duplicate generations within the same generation request was fixed, but it's still possible that duplicates can exist between two separate generation requests. I plan on adding a duplicate card detection system in the future to prevent this.
Features on the Roadmap
- Adding more SSO flows
- Multiple flashcard layouts to choose from
- Self-hosting LLMs to reduce token cost and environmental impact
- Varying LLMs for varying languages to ensure better accuracy in non-Romance or Germanic languages
XII. Conclusion
Key Takeaways
When creating a side-project, think of some functionality that YOU would use or need. Design decisions become much clearer when you yourself are a user of the product. Creating an app from front to back allows a better understanding of the software development lifecycle.
Resources
- Next.js docs
- .NET docs
- OpenAI API docs
- Stripe docs
- Google OAuth docs
Final Thoughts
Creating this app has taught me many things, allowed me to stretch my development muscles, and revealed my weak spots as a developer. I now have a better understanding of the full software development lifecycle, which is crucial for growth in the industry. Setting up the hosting services and deployment automation revealed how much I don't know, while allowing me to dip my toes into devops. This was the major contributing factor that cleared my vision for pursuing a DevOps Engineer certificate in the near future.
Try LanguageDecks and give your feedback!