Backend Lab
A real API, running live.
Every widget on this page talks to my Django REST Framework backend. Nothing is mocked: you see the actual request, status code, timing and JSON response.
01
Contact API
POST /api/contact/The portfolio's contact form posts here. A DRF ModelSerializer validates the name, email, optional phone and message, then the message is saved to PostgreSQL and shows up in the Django admin.
- Rate limited with DRF
ScopedRateThrottle: 5 accepted messages per hour per IP. The 6th returns429with aRetry-Afterheader. A custom throttle counts only submissions that pass validation, so typos never lock a visitor out. - Honeypot: a hidden
websitefield that only bots fill in. They get a fake201, and nothing is stored or emailed. - After the database commit, a Resend email notification is sent in the background, so a slow email API never slows down the response.
"Valid message" really saves a row (rate-limited to 5/hour per IP).
02
Projects API
GET /api/projects/GET /api/projects/{slug}/The Projects section is served from this endpoint. Projects are stored in the database and edited in the Django admin (ordering, featured flag, publish toggle), so updating the portfolio doesn't need a redeploy.
The site is pre-rendered with a static copy of the list, so it loads instantly and still works if the API is asleep. The live data replaces it once the API responds.
03
Guestbook
GET /api/guestbook/?page=nPOST /api/guestbook/Leave a short note (up to 280 characters). The list is paginated with DRF's PageNumberPagination, 5 entries per page, newest first.
- Validation: name 2–40 characters, message 3–280 characters.
- Moderation flag: entries with links are automatically held back (
is_approved=False) until I approve them in the admin. - Separate throttle for writes (10/hour), plus the same honeypot as the contact form.
04
Live stats
POST /api/stats/hit/GET /api/stats/Each page load sends an anonymous POST /api/stats/hit/. Visitors are counted with a salted, daily-rotating SHA-256 hash, so no raw IP address is ever stored.
The aggregate query is cached for 60 seconds in Django's database cache, which is shared across gunicorn workers without Redis. Click refresh twice: the second response says "cached": true and comes with an X-Cache: HIT header.
05
Interactive API explorer
any /api/* endpointA small Postman in the browser. Pick a preset or write your own request, press Send, and you'll see the method, status code, total time (plus the server's own processing time from the X-Response-Time-Ms header), payload size and the JSON response.
Get a JWT with the "Get JWT" preset or in the auth demo below, then tick Bearer token to call protected endpoints.
Base URL: https://portfolio-api-ljpc.onrender.com
Press Send to make a request.
06
JWT auth demo
POST /api/auth/token/POST /api/auth/token/refresh/GET /api/auth/me/Stateless authentication with djangorestframework-simplejwt. Logging in returns a short-lived access token (10 minutes) and a refresh token (12 hours). The protected endpoint only answers when it gets Authorization: Bearer <access>.
The demo account is public and has no admin rights. It is re-created on every deploy, and login attempts are throttled.
07
Auto-generated API docs
GET /api/docs/GET /api/redoc/GET /api/schema/drf-spectacular introspects the serializers and views to generate an OpenAPI 3 schema, served through Swagger UI and ReDoc. Request and response shapes, throttles and auth stay in sync with the code automatically.
08
Health & status
GET /api/health/GET /api/health/?deep=1A plain liveness endpoint. It drives the "API online" indicator in the navbar and footer, and Render uses it as the service health check.
By default it doesn't touch the database, so uptime checks don't keep the serverless Neon Postgres awake (that would use up its free compute hours). Add ?deep=1 to also run SELECT 1.
State
Unknown
Uptime
–
Version
–
09
Tests & CI
pytestGitHub ActionsThe API is covered by 37 pytest tests (pytest-django + DRF's APIClient). On every push, a GitHub Actions workflow runs them on Python 3.13 and 3.14 against a real PostgreSQL 17 service container. It also runs manage.py check --deploy and checks that no migrations are missing.
Task queues: the Coursera specialization covered background jobs with Celery + Redis. The free hosting tier has no worker processes, so here the email notification runs in a lightweight background thread after the database commit. With a paid worker it would be a Celery task.
- test_contact.py · validation, honeypot, accepted-only 429 throttling, Resend notification (mocked)
- test_projects.py · ordering, publish flag, featured filter, detail
- test_guestbook.py · pagination, link moderation, validation, honeypot
- test_core.py · health (zero DB queries), stats caching, hashed visitors, OpenAPI
- test_auth.py · JWT login/refresh, 401s, demo user rights, superuser bootstrap