ID: S202601171410
Status: school
Tags: Avans 2-1 LU3
Svelte + Nestjs + MariaDB + FastAPI
Our project exists out of 3 repositories, the Svelte frontend that you are in right now. The python FastAPI for our AI. The NestJs backend that runs most of our logic. You can find all the repos under our github organisation.
Local setup
This is the section where I explain how to run it all locally.
Maria DB There are different ways to host MariaDB. I personally run a docker container:
docker run --detach --name some-mariadb --env MARIADB_ROOT_PASSWORD=my-secret-pw mariadb:latestThis is the command gathered from the docker hub, on production you might want to change the username as well, but since it is just locally, it doesn’t really matter.
We personally all use dbeaver.io to connect to our database. But that is not a required step if you jsut want to run the app locally.
Python FastAPI
Running the FastAPI requires a few steps.
First off, running it locally will require a .env that looks something like this:
#DATABASE CONFIGURATION (MariaDB / MySQL)
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=my-secret-pw
DB_NAME=db_naam
RUNNING_PORT=8000Then you have to install the dependencies:
pip install -r requirements.txtpython -m spacy download nl_core_news_smpython -m venv .venv.venv\Scripts\activate
And then you can run it with uvicorn app.main:app --reload, or with uvicorn app.main:app --reload --port 6767 if your port 8000 is already taken.
THen you should see something like this in your terminal:
INFO: Will watch for changes in these directories: ['Avans-KeuzeModule-App-AI']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [67098] using StatReload
INFO: Started server process [67100]
INFO: Waiting for application startup.
2026-01-17 10:31:22,566 | INFO | ai-recommender | FastAPI application has started.
2026-01-17 10:31:22,566 | INFO | ai-recommender | Dataset path resolved to: /Avans-KeuzeModule-App-AI/app/data/Uitgebreide_VKM_dataset.csv
2026-01-17 10:31:22,566 | INFO | ai-recommender | Loading dataset from /Avans-KeuzeModule-App-AI/app/data/Uitgebreide_VKM_dataset.csv
2026-01-17 10:31:22,570 | INFO | ai-recommender | Dataset loaded: 211 rows
2026-01-17 10:31:26,280 | INFO | ai-recommender | Recommender Engine Prepared!
2026-01-17 10:31:26,280 | INFO | ai-recommender | Recommender Engine has been initialized.
INFO: Application startup complete.You have the FastAPI running!
NestJs Backend
First off, make sure to run npm i to install all packages.
For running the NestJS backend locally, you are going to need a .env like this:
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=my-secret-pw
DB_NAME=db_naam
PORT=3000
FASTAPI_URL=http://localhost:8000/ai
RECOMMENDATION_CACHE_HOURS=24
BCRYPT_ROUNDS=12
DUMMY_HASH=
COOKIE_SECURE=true
COOKIE_SAMESITE=strict
AUTH_COOKIE_ACCESS=access_token
AUTH_COOKIE_REFRESH=refresh_token
ACCESS_TOKEN_EXPIRES_IN_SECONDS=900
REFRESH_TOKEN_EXPIRES_IN_SECONDS=604800
JWT_SECRET=
CORS_ORIGINS=http://localhost:5173
DISCORD_TOKEN=
CHANNEL_ID=PORTis by default 3000, even if the value is not present in the.env. But if you already have anodejsapp, it is most likely taken, so you are able to change it.DUMMY_HASHis just a dummy hash, so that we even compare a hash when a huser doesn’t exist, to prevent hackers from enumerating our login process.JWT_SECRETis a dummy JWT token, for the same reason as we have ourdummy hash.CORS_ORIGINSpoints to our svelte app that will run at port5173DISCORD_TOKENis for discord bot intergration, this is not needed to run the backend.CHANNEL_IDis for discord bot intergration, this is not needed to run the backend.
Then to run the backend, run npm run start or npm run start:dev.
You’ll know it started correctly if you see:
AVANS KEUZEMODULE BACKEND has started.
Listening on http://127.0.0.1:3000...If it is followed by some JSON messages in the console, those are warings and errors. Those shouldn’t happen unless you are using it’s endpoints. If it does happen directly after startup, then please look at your .env.
Svelte Frontend
First off, make sure to run npm i to install all packages.
To run your svelte with the above setup locally, you’ll need a .env like this:
VITE_API_BASE_URL=http://localhost:3000/api
TEST_USER_EMAIL=playwright@obviously-fake-mail.nl
TEST_USER_PASSWORD=This one is quite straightforward, the VITE_API_BASE_URL just points to our NestJs.
TEST_USER_EMAIL and TEST_USER_PASSWORD are the credentials of a user that you will use to run tests, this is not needed for running the app.
You run the app locally with npm run dev. And you should see something like this:
VITE v7.3.0 ready in 204 ms
âžś Local: http://localhost:5173/
âžś Network: use --host to expose
âžś press h + enter to show help
Now you are able to go to http://localhost:5173/ and experience our app.
Hosting
This is the section where I explain how to host it on a VPS. We personally use a contabo VPS, but any VPS that runs docker will do. Below is our full setup.
Portainer
We personally use Portainer for managing docker containers. But it is just an UI for docker, anything you are able to do with portainer is also possible without it.
Maria DB There are different ways to host MariaDB. We run a docker container from within our portainer. You can look up how to do this on docker hub. You could run the same command as shown how to run it locally, but since you are running this live, it would be securer to never expose this port to the outside world, and to only allow local network to it. And then use a SSH tunnel if you want to look in the database with an app like dbeaver.io. It is also good practice to not use root as username.
Python FastAPI
We personally deploy this by merging into main on github via CICD, but you can also do it with the following command:
docker build -t fastapi-app
docker run --rm -p 8000:8000 --env-file .env fastapi-appNestJs Backend
We personally deploy this by merging into main on github via CICD, but you can also do it with the following command:
docker build -t nestjs-app .
docker run --rm -p 3000:3000 --env-file .env nestjs-appSvelte Frontend
We personally deploy this by merging into main on github via CICD, but you can also do it with the following command:
docker build -t svelte-app .
docker run --rm -p 5173:5173 svelte-appNginx
We run Nginx as a small “edge” reverse proxy in Docker (edge-nginx) that sits in front of the application on the VPS. It exposes ports 80 and 443 to the internet: all HTTP traffic on port 80 is automatically redirected to HTTPS, and HTTPS is terminated by Nginx using the certificate files we mount into the container (/opt/edge-nginx/certs → /etc/nginx/certs). From there, Nginx forwards requests to the correct internal service over the shared Docker network (app-network): requests to /api go to the backend (avans-keuzemodule-backend:3000), requests to /ai go to the AI service (avans-keuzemodule-api:8000), and everything else is forwarded to the frontend SPA (avans-keuzemodule-frontend:80).
Testing
We have a lot of tests in our containers. These tests are ran in our CICD pipeline before the containers are being deployed. You can also run these tests locally.
Nestjs
To run the tests in the backend, simply use npm run test. This will run our unit and end2end tests.
Svelte To run the playwright tests, you can choose different approaches:
- Run them in your terminal with
npm test - To open a GUI where you can test each test manually and see the steps and progress, run
npm run test:ui - You can also unstall the vscode plugin
Discord bot intergration
Our app has a discord bot intergration. This allows our NestJs application to send alert messages whenever we get a WARNING or ERROR or CRITICAL log in our system.
- You would set this up by creating a discord bot through the developer portal.
- Then you set it’s
installation contextin theinstallationsettings toGuild install. And setDefualt install settingsto includebotin thescopesinput field. - You invite the bot into your sever with the install link.
- in the tab
boton the developer portal, reset thetokenand copy it into theDISCORD_TOKENfield in your.env. - in your discord server, right click on a channel, and copy the link. It will look something like
https://discord.com/channels/0000000000000000000/0000000000000000000. THe second number is the channel ID that you need to paste in your.envbehindCHANNEL_ID.
You can test the discord bot by going into the admin panel of the Svelte frontend, going to the logs page and clicking the archive button.
Logs
The NestJs backend logs almost everything, and it does this in JSON format. This makes it a little bit harder to read from the terminal, but luckily there are alternatives.
On the admin panel, there is a logs page. This logs page comes with all the logs we have. But you can filter on it. We re-invented the mongodb-like syntax so that you can filter on anything you would like to look at. By defaut you can filter on:
- the last hour
- warnings and errors
- security alerts
- a user ID
But you can change this to be really specific, like:
{
"$and": [
{"message.level": {"$in": ["error", "warn"]}},
{"timestamp": {"$gte": "2026-01-15T00:00:00.000Z"}},
{"$or": [
{"message.securityAlert": true},
{"message.httpResponse": {"$gte": 500}}
]}
]
}This example query finds logs that are errors OR warnings, from today or later, AND either marked as security alerts OR have server error status codes (500+).
You can save your own custom prefabs to make it usefull for everyone. This is saved in localstorage.
Log Status
By default, the logs that you’ll see will have the log status marked as closed. This is because all prefabs filter the other options away.
Our logger makes it so that we can create a log at the start of a users GET request, update it with more data whilst it goes through our systems flow, and send it once it is done, or gets an error. This makes it really nice to debug errors and issues with. The closed status are all logs that reached that final moment where we have sent the log. But we also write a log with status init on creation. And sometimes we write a log with partial as status when we are waiting for an API requist midway through a flow, this way we can atleast see that the flow has made it to a specific point in our code.
All the logs, from init to closed get assigned an logId so that we can see them change over time.