- Web
LMS Academy
A learning platform where training academies sell and teach online courses and digital products, and students buy, learn, take quizzes, send projects and book live sessions in one place.
- Personal project, full-stack
- 85 API endpoints, 17 database tables
- 498 unit tests, all passing
- Next.js, React, TypeScript, Tailwind CSS, NestJS, PostgreSQL, Prisma, Jest

The problem
Training academies need one place to sell online courses and downloadable resources, teach them with video lessons, quizzes, projects, questions and live sessions, and follow their students. Students need one place to buy, learn and get feedback.
The approach
An academy builds courses from modules and ordered video lessons, adds quizzes and a final project, and sells the courses and digital products in Tunisian dinars, with discount coupons. Students follow the lessons in a course player, take module quizzes, ask questions under each lesson, send their project for review and book live sessions with the academy. A platform admin creates the academies and chooses their admins.
The browser loads the Next.js web app, whose pages call the NestJS REST API with a sign-in token. The API runs every request through the same checks, then a service applies the rule and talks to PostgreSQL through Prisma.
Who uses it
- Students
- Buy courses and products, follow the lessons, take quizzes, send projects, ask questions and book live sessions.
- Academy admins
- Build and sell their academy's courses, confirm payments, review projects, answer questions and run sessions.
- Platform admins
- Create the academies, choose their admins and help with password resets.
What it does
- A public course catalog with search, and a page per course with its modules and lessons
- An academies directory, and a page per academy with its courses
- A store of digital products, such as guides, templates and source code, sold as download links
- Checkout with discount coupons, with payments confirmed by hand by the academy
- A course player with video lessons, lessons marked as done and progress per course
- Module quizzes with multiple-choice and true/false questions, attempts and a best score
- Questions under each lesson, with replies and an accepted answer
- Course projects that students send with their repository and live links, and that the academy approves or sends back
- Live sessions: students request mentoring or webinars, and the academy confirms them and adds the meeting link
- An academy admin space with a dashboard, a course editor, students with a CSV download, orders, coupons and products
- A platform admin space for academies, their admins and password reset links
- API documentation with Swagger while developing
Where the engineering is
Rights that change at once
A sign-in token issued before a role change or a password change must not keep its old rights. The API reads the account and its role from the database on every request and compares the version number carried by the token, so changing the password, or choosing to sign out everywhere else, makes the older tokens stop working.
Passwords are hashed with argon2, and the three roles are checked by guards on every request.
Money in dinars, counted once
Prices are decimal amounts in Tunisian dinars. Every computed amount, such as a coupon discount or an order total, is rounded to the millime, and a price accepts at most three decimals.
Payments are confirmed by hand by the academy. Marking an order as paid is one conditional update inside a transaction, so two admins cannot confirm it twice and a coupon use is counted only once.
, Open the full-size image (opens in a new tab)One platform, many academies
Several academies share one platform, so every admin query goes through the academy of the admin who makes it. An academy never sees or changes another academy's courses, students or orders.
The data lives in 17 related tables, from courses, modules and lessons to orders, coupons and bookings, with foreign keys between them and 14 migrations that are only ever added, never reset.
, Open the full-size image (opens in a new tab)Every request through the same checks
The API checks each request in a fixed order: security headers, the list of allowed sites, a rate limit of 300 requests a minute and 10 for sign-in and sign-up, the token, the role, and then the body. Bodies are validated by 32 DTO classes: unknown fields are refused and HTML is removed.
The environment variables are validated when the API starts, and the Swagger documentation of the 85 endpoints is available while developing but off in production unless asked for.
, Open the full-size image (opens in a new tab)A local database that is safe to set up
The setup script first checks that the database belongs to this app, and refuses one that holds another project's tables. Then it applies only the migrations that are missing.
Screens
, Open the full-size image (opens in a new tab)Creating an account, with the current terms.
, Open the full-size image (opens in a new tab)A broken reset link explains what to do.
, Open the full-size image (opens in a new tab)The web app test run.
, Open the full-size image (opens in a new tab)The tests of the setup scripts.
How it's built
| Layer | Technology |
|---|---|
| Web app | Next.js 16 and React 19: 44 page files and 21 components, with one dark theme |
| Styling | Tailwind CSS 4, with a visible keyboard focus and text colors that meet WCAG AA contrast |
| API | NestJS 11 with 85 endpoints in 16 controllers, and guards and pipes for sign-in, roles and validation |
| Database | PostgreSQL with Prisma 7: 17 tables, 7 enums and 14 migrations |
| Security | JWT with a version number, argon2 password hashing, Helmet headers, rate limits and HTML cleaning |
| Tests | Jest for the API and the Node.js test runner for the web app, with Prisma replaced by stubs |
, Open the full-size image (opens in a new tab)Testing
498 unit tests pass: 352 for the API in 76 files, 136 for the web app and 10 for the scripts. They run without a database, with Prisma replaced by stubs.
A route check also follows every internal link of the web app: 44 pages and 114 links.
Results
- 44 page files in the web app and 21 components.
- 85 API endpoints in 16 controllers.
- 17 database tables and 7 enums, changed by 14 migrations.
- 498 unit tests, all passing on the last run.
What I would build next
- Online payments, instead of an academy confirming each payment by hand.
- Emails for password resets, instead of a link made by the platform admin.
- End-to-end tests for buying, learning and reviewing.
- Pagination for the lists that are loaded whole.
Built with
- Next.js
- React
- TypeScript
- Tailwind CSS
- NestJS
- PostgreSQL
- Prisma
- Jest