This is the backend for my little corner of the web.
It is a small Express app that handles the practical side of the site: blog posts, comments, likes, Discord login, profile updates, anime data, image uploads, quote snapshots, sitemap generation, and a few SEO-friendly routes for sharing pages nicely.
The frontend gets most of the cute attention, but this is the part quietly doing the real work in the background. It stores the data, serves the images, handles auth, keeps the blog editable, and makes sure the site still works like an actual app instead of just being a pretty page.
- Blog post CRUD routes
- Comment and like handling
- Guestbook entry, position, and moderation routes
- Discord OAuth login and session tokens
- Profile update routes
- Anime feed routes
- MyAnimeList currently-watching snapshot sync
- Daily quote snapshot storage and fetching
- Image upload and optimization
- Sitemap and IndexNow helpers
mirabellier-backend/
|- app.js Main server entry
|- routes/ Route handlers for posts, auth, anime, guestbook, images, quotes
|- lib/ Database, uploads, users, sitemap, quote helpers
|- images/ Uploaded images
|- scripts/ Utility scripts
|- database.sqlite3 Local SQLite database
`- package.json Backend scripts
- Node.js
- Express 5
- SQLite with
better-sqlite3 - Passport Discord
- Multer
- Sharp
cd mirabellier-backend
npm installUse .env.example as your starting point.
PORT=3000
DB_FILE=./database.sqlite3
SESSION_SECRET=your-very-secret-value
IMAGES_DIR=images
DISCORD_CLIENT_ID=your_discord_client_id
DISCORD_CLIENT_SECRET=your_discord_client_secret
DISCORD_CALLBACK_URL=http://localhost:3000/auth/discord/callback
FRONTEND_URL=http://localhost:5173
MAL_CLIENT_ID=your_myanimelist_client_id
MAL_USERNAME=your_myanimelist_username
MAL_ANIME_REFRESH_MINUTES=5
WEBSITE_BASE=https://mirabellier.com
INDEXNOW_KEY=your-indexnow-key
QOTD_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...For development:
npm run devFor a regular run:
npm startWith the example .env above, the backend will run at http://localhost:3000. If PORT is unset, app.js falls back to 5000.
npm run dev- start the backend with nodemonnpm start- start the backend normallynpm run generate:sitemap- regenerate sitemap data
- The SQLite database is initialized automatically on startup
- Uploaded images get optimized with Sharp
- Quote data is stored as snapshots instead of being scraped every time
- The API supports anonymous likes as well as logged-in likes
- Guestbook note positions are stored in SQLite, so moving a note syncs for other visitors
- The owner account can moderate guestbook notes
- MyAnimeList currently-watching data is cached in SQLite so the public page can survive MAL outages
- The server generates SEO-friendly responses for shared blog, profile, anime, question-of-the-day, and quote links
- Sitemap and IndexNow helpers are built in so new posts can be surfaced faster
- Question of the Day can post one Discord webhook notification per live drop
The public /anime page can sync directly from a public MyAnimeList profile.
Set these env vars in mirabellier-backend/.env:
MAL_CLIENT_IDfrom your MyAnimeList API appMAL_USERNAMEfor the public profile you want to mirror- optional
MAL_ANIME_REFRESH_MINUTESto change the backend refresh window from the default 5 minutes
This v1 uses the public username endpoint with the X-MAL-CLIENT-ID header. It does not use MAL OAuth.
The backend stores the last successful normalized snapshot in SQLite, serves that snapshot while it is fresh, refreshes it when stale, and falls back to the last successful result with stale: true if MAL is temporarily unavailable.
If you want a Discord channel ping whenever a new Question of the Day goes live:
- Open your Discord server channel settings.
- Go to
Integrations > Webhooks. - Create a webhook for the channel you want.
- Copy the webhook URL into
mirabellier-backend/.envasQOTD_DISCORD_WEBHOOK_URL.
Optional env vars:
QOTD_DISCORD_WEBHOOK_USERNAMEto change the webhook display nameQOTD_DISCORD_WEBHOOK_AVATAR_URLto change the webhook avatar
The backend only posts once per question, stores that state in SQLite, checks again on startup, and keeps a lightweight background check running so UTC-day rollovers still notify even without an admin action.
GET /posts- list blog postsGET /posts/:id- fetch one postPOST /posts- create a postPUT /posts/:id- update a postDELETE /posts/:id- delete a postPOST /posts/:id/comments- add a commentPOST /posts/:id/like- like or unlike a postGET /guestbook- list guestbook notesPOST /guestbook- create a guestbook notePATCH /guestbook/:id/position- save a note position on the boardDELETE /guestbook/:id- delete a guestbook note as the owner accountPOST /posts-img- upload an imageGET /anime- SEO/share page for the public anime routeGET /anime/currently-watching- fetch the live MyAnimeList-backed currently watching feedGET /anime/currently-watching/embed-image.png- render the public anime share preview imageGET /question-of-the-day- SEO/share page for the public question-of-the-day routeGET /question-of-the-day/embed-image.png- render the public question-of-the-day share preview imageGET /quotes- SEO/share page for the public quotes routeGET /quotes/embed-image.png- render the public quotes share preview imageGET /quote-of-the-day- fetch a daily quote snapshotGET /auth/discord- start Discord OAuthGET /auth/discord/callback- finish Discord OAuthGET /me- fetch the current userPOST /me- update the current user profilePOST /logout- destroy the current sessionGET /user/:id- fetch a public user profile by idGET /user/by-username/:username- fetch a public user profile by usernameGET /user/:id/stats- fetch public stats for a user
- Check your
.envfirst - If Discord login fails, the callback URL is usually the first thing to verify
- If uploads fail, make sure
IMAGES_DIRis writable and Sharp installed correctly - If data seems stale or locked, make sure only one local server is using the SQLite file
- If frontend auth redirects look wrong, check
FRONTEND_URL - If guestbook note positions look clipped, make sure the backend is running the current board-size constants
I wanted the backend to stay small enough to understand, but capable enough to support the whole site properly. It is not trying to be fancy for the sake of it. It just needs to be dependable, readable, and easy to extend whenever I add another little feature to the site.
That is the whole mood of this backend: quiet, useful, and doing a lot more than it shows.
