The backend moves from NestJS 11 on Vercel serverless to FastAPI on Python 3.13, hosted on FastAPI Cloud, and gains two new resources. This is an architectural migration, not a framework swap: the same MongoDB cluster and the same collections are read, and nothing was re-seeded.
The cutover is complete. https://api.dileepa.dev is served by FastAPI Cloud against the production database, and the health check is /health.
Important
This is a single cutover, not a staged one. The v1 paths that are gone return 404 from the moment this ships, and the consumers move with it — nothing is carried behind a deprecation.
No password reset is required. The JWT algorithm, secret and claim names are unchanged, and Node bcrypt hashes verify as-is and are rewritten to argon2id on the next sign-in. A token minted by the NestJS app is still accepted, so a session live at the cutover was not signed out.
Breaking changes
| v1 endpoint | Successor |
|---|---|
POST /auth/sign-in |
POST /auth/login — same body, same token shape |
POST GET DELETE /upload |
the /uploads equivalents |
- Collection endpoints return an envelope,
{ items, total, limit, offset }, rather than a bare array. - An empty collection is
200with an empty list, not404. v1 threwNotFoundExceptionwhen a list came back empty, which made an empty section indistinguishable from a broken endpoint. - Errors return
{ error: { code, message, details } }on every endpoint, replacing{ statusCode, timestamp, path, message }. - Records expose
id, not_id, and never__v.indexis noworder— the API reads either name. /eventsis reshaped, keeping its path and collection name.- Blog posts carry a relative
pathand a composedcanonicalUrlinstead of an absoluteblog.dileepa.devlink, a realpublishedDateinstead of a date string, anddescriptioninstead ofexcerpt.
Added
New resources — /projects (net-new: slug, status, period, stack, gallery, metrics, SEO), /pillars and /speaking-topics (site copy that was previously compiled into the website), GET /api-links (the endpoint catalogue, derived from the live dependency graph rather than hand-maintained), GET /status, GET /health, GET /version, and GET/POST /maintenance/database behind five layered guards and unregistered in production.
Blog engagement — views de-duplicated per reader per 24 hours through a unique index and a TTL rather than a check in the handler, four reactions, comments with one level of replies, comment reactions, admin-only moderation, and a denormalised commentCount maintained with $inc.
Auth — POST /auth/refresh and GET /auth/profile, with refresh tokens alongside access tokens.
Also — PATCH /{resource}/order for bulk reordering, ?hasPhotos= on /events, description on videos, location/shortBio/fullBio on about, and contact submissions persisted to the database in addition to the Resend notification.
Security
Eight security headers on every response, verified against the live domain. CORS is an allowlist, never a wildcard. Startup refuses a misconfigured production — a placeholder JWT_SECRET, a localhost MONGODB_URI, a wildcard CORS_ORIGINS or an empty BLOG_SYNC_API_KEY each abort the boot. Errors never carry internals, and sign-in failures do not distinguish an unknown address from a wrong password. The API reference and its spec are unregistered in production, so neither /docs nor /api-json exists to be found.
Fixed
Four defects found by the pre-release review, each pinned by a test:
- Rate limiting failed open. Since FastAPI 0.141,
include_routerkeeps routers nested, so slowapi's middleware found no endpoint to read a limit from and appliedRATE_LIMIT_DEFAULTto nothing —/auth/loginincluded, with no429and noX-RateLimit-*header to notice. - The strictest limits were the only bypassable ones. A
@limiter.limitdecorator is checked inside the endpoint and FastAPI validates the body first, so a malformed request consumed no budget — leaving/contactand the comment routes, the two tightest configured limits, with no limit at all on invalid input. - A disabled account was identifiable without its password.
account_disabledwas returned before password verification, answering "is this address registered here?" for anyone who asked. - Comment moderation required only a valid token, not the
adminrole — four routes that return commenter email addresses and write owner-badged replies.
Also: an oversized upload is rejected without being read into memory first, and a slug miss no longer costs two identical queries.
Removed
The NestJS application — src/, test/, package.json and the whole Node toolchain, 95 files. Azure Blob Storage; Cloudinary is the only image backend. The legacy field on blog rows, archived into blogs_v1_legacy_backup first.
Nothing is deprecated. tests/contract/test_v1_parity.py records every dropped v1 route with its successor, and tests/test_openapi.py fails if any operation is ever published with deprecated: true.
Verified at release
/health reports the database up · /version reports 2.0.0 in production · /docs and /api-json return 404 · all eight security headers present · /pillars 6, /speaking-topics 4, /projects 7, /events 30, /blogs 22 · 424 tests passing, ruff and mypy --strict clean.
Full changelog: CHANGELOG.md · Scope: #13 · Compare: v1.2.1...v2.0.0