Skip to content

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.

Django 5.2Django REST FrameworkSimpleJWTdrf-spectacularPostgreSQL (Neon)WhiteNoisegunicornRender

API: https://portfolio-api-ljpc.onrender.com/api/

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 returns 429 with a Retry-After header. A custom throttle counts only submissions that pass validation, so typos never lock a visitor out.
  • Honeypot: a hidden website field that only bots fill in. They get a fake 201, 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).

No request yet. Use the demo to send one.

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.

No request yet. Use the demo to send one.

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.
No request yet. Use the demo to send one.

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.

No request yet. Use the demo to send one.

05

Interactive API explorer

any /api/* endpoint

A 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.

No request yet. Use the demo to send one.

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=1

A 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

–

No request yet. Use the demo to send one.

09

Tests & CI

pytestGitHub Actions

The 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
View CI runs