Skip to content

Repository files navigation

Coderr Backend

Backend for Coderr, a freelancer marketplace where business users publish offers and customer users order them and write reviews. Business users create tiered offers (basic / standard / premium); customers place orders from an offer's detail tiers and leave one review per business. This repository contains the backend only; the frontend is a separate, delivered static application served on http://127.0.0.1:5500.

  • Stack: Python, Django, Django REST Framework, Django ORM, SQLite.
  • Auth: DRF TokenAuthentication. Login uses username + password (never email). No JWT.
  • API base URL: http://127.0.0.1:8000/api/ locally, https://yannick-oetelshoven.developerakademie.org/api/ in production (see Production deployment).

Prerequisites

  • Python 3.12
  • pip and the ability to create a virtual environment
  • Git

Setup from a fresh clone

# 1. Clone the repository
git clone <repository-url>
cd Coderr-Backend

# 2. Create and activate a virtual environment
python3.12 -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\activate

# 3. Install dependencies
pip install -r requirements.txt

# 4. Create the environment file from the template
cp .env.example .env
# Then generate a real secret key and paste it into .env:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

# 5. Apply database migrations
python manage.py migrate

# 6. Create an administrator account
python manage.py createsuperuser

# 7. Run the development server
python manage.py runserver           # http://127.0.0.1:8000

The database (db.sqlite3) is not in the repository, so a fresh clone starts with an empty database. Create data via the API (see Demo accounts below) or the Django admin.


Environment variables

Configuration is loaded from a local .env file via python-dotenv. See .env.example for the required keys. Secrets are never committed.

Variable Description Example
SECRET_KEY Django secret key (required, no default) (generated value)
DEBUG Debug mode, True or False True
ALLOWED_HOSTS Comma-separated list of allowed hosts 127.0.0.1,localhost
CSRF_TRUSTED_ORIGINS Comma-separated origins with scheme, empty by default https://your-domain.example
CORS_ALLOWED_ORIGINS Comma-separated origins with scheme, dev origins by default http://127.0.0.1:5500

The application refuses to start if SECRET_KEY is missing — there is no hardcoded fallback.


Authentication

All authenticated requests carry a DRF token in the Authorization header, using the literal word Token and a single space:

Authorization: Token 83bf098723b08f7b23429u0fv8274

Tokens are issued on login and registration, which both return token, username, email and user_id. Login is performed with username and password; email is never used as a credential.


API overview

Base URL: http://127.0.0.1:8000/api/

Method Path Who may call it
POST /api/registration/ Anyone
POST /api/login/ Anyone
GET /api/profile/{pk}/ Any authenticated user
PATCH /api/profile/{pk}/ The profile's owner
GET /api/profiles/business/ Any authenticated user
GET /api/profiles/customer/ Any authenticated user
GET /api/offers/ Anyone (public, paginated)
POST /api/offers/ Authenticated business users
GET /api/offers/{id}/ Any authenticated user
PATCH /api/offers/{id}/ The offer's owner
DELETE /api/offers/{id}/ The offer's owner
GET /api/offerdetails/{id}/ Any authenticated user
GET /api/orders/ Any authenticated user (own orders)
POST /api/orders/ Authenticated customer users
PATCH /api/orders/{id}/ The order's assigned business user
DELETE /api/orders/{id}/ Staff (admin) users
GET /api/order-count/{business_user_id}/ Any authenticated user
GET /api/completed-order-count/{business_user_id}/ Any authenticated user
GET /api/reviews/ Any authenticated user
POST /api/reviews/ Authenticated customer users
PATCH /api/reviews/{id}/ The review's author
DELETE /api/reviews/{id}/ The review's author
GET /api/base-info/ Anyone (public)

Known specifics

  • Pagination is applied only to the offer list, with page size 6. GET /api/offers/ returns {count, next, previous, results}; every other list endpoint (profiles/business/, profiles/customer/, orders/, reviews/) returns a bare array. The project does not set DEFAULT_PAGINATION_CLASS in REST_FRAMEWORK; doing so would wrap those bare-array responses and break the frontend, which assigns them directly to arrays.
  • Profile text fields serialize as empty strings, never null. first_name, last_name, location, tel, description and working_hours are always strings; the frontend renders them directly.
  • Orders store a snapshot of the offer detail. At creation an order copies the title, revisions, delivery time, price, features and offer type; it holds no foreign key to the offer detail, so later edits or deletion of the source offer leave existing orders unchanged.
  • The database is not in the repository. A fresh clone starts with an empty database; there are no fixtures.
  • Filtering is configured per view, not through a global DEFAULT_FILTER_BACKENDS.
  • Media files are served by Django only while DEBUG is True, via MEDIA_URL and MEDIA_ROOT, because the frontend prefixes relative media paths with http://127.0.0.1:8000/.
  • CORS defaults to the frontend dev origins http://127.0.0.1:5500 and http://localhost:5500, and is replaced entirely by CORS_ALLOWED_ORIGINS when that variable is set.
  • Four views declare permissions through get_permissions() rather than a permission_classes attribute — OfferListCreateView, OrderListCreateView, OrderStatusUpdateDeleteView and ReviewListCreateView. On those paths the HTTP methods (e.g. GET vs POST) require different permissions, which a single permission_classes list cannot express. Every method returns an explicit permission list; no view inherits the global default silently.
  • The offer list drives one extra request per offer detail, by design. The delivered frontend fetches each detail separately, so a page of six offers produces eighteen additional requests to /api/offerdetails/{id}/. This is client behaviour and is not fixed in the backend; each offerdetails/{id}/ call is kept to a single query.
  • The delivered frontend redirects away from login.html and registration.html whenever any token is present in localStorage, without checking its validity (login.js:7, registration.js:7). A client holding a foreign or expired token therefore cannot reach the login form until localStorage is cleared. The backend keeps all public endpoints reachable in that state; the client-side guard itself is frontend behaviour and is not modified.

Demo accounts

The delivered frontend hardcodes two guest logins in its config.js:

Type Username Password
customer andrey asdasd
business kevin asdasd24

These accounts do not exist in a fresh database — the frontend only sends their credentials to POST /api/login/; it never creates them. Recreate them once against a running server with two registration calls (any email works):

curl -X POST http://127.0.0.1:8000/api/registration/ \
  -H "Content-Type: application/json" \
  -d '{"username": "andrey", "email": "andrey@example.com", "password": "asdasd", "repeated_password": "asdasd", "type": "customer"}'

curl -X POST http://127.0.0.1:8000/api/registration/ \
  -H "Content-Type: application/json" \
  -d '{"username": "kevin", "email": "kevin@example.com", "password": "asdasd24", "repeated_password": "asdasd24", "type": "business"}'

The kevin business account should own at least one offer (create one with POST /api/offers/ while logged in as kevin), otherwise the page looks empty after a business guest login.


Development commands

source .venv/bin/activate
python manage.py runserver
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
ruff format . && ruff check .
python manage.py test
coverage run manage.py test && coverage report -m

Production deployment

The backend runs live at https://yannick-oetelshoven.developerakademie.org.

Nginx terminates TLS and proxies to Gunicorn over plain HTTP on 127.0.0.1:8000; Supervisor keeps Gunicorn alive. The frontend and the API are served from the same origin — the frontend is a plain static site under /, the API lives under /api/ on that same host — so the browser never issues a cross-origin request and CORS plays no part in this deployment.

Server layout

Path Contents
/var/www/coderr/backend This repository
/var/www/coderr/backend/env Virtual environment
/var/www/coderr/frontend Delivered static frontend
/var/log/coderr/ Gunicorn stdout and stderr logs

The virtual environment is called env on the server, not .venv as in local development:

source /var/www/coderr/backend/env/bin/activate

Process management (Supervisor)

Supervisor runs Gunicorn as the program coderr_gunicorn, defined in /etc/supervisor/conf.d/coderr-gunicorn.conf, bound to 127.0.0.1:8000 and writing its logs to /var/log/coderr/.

sudo supervisorctl status coderr_gunicorn
sudo supervisorctl restart coderr_gunicorn
sudo tail -f /var/log/coderr/gunicorn-stderr.log

After editing the Supervisor config itself, reload it with sudo supervisorctl reread && sudo supervisorctl update.

Gunicorn runs with 3 workers rather than the (2 * cores) + 1 the course material prescribes — see DEVIATIONS.md.

Nginx

The site is defined in /etc/nginx/sites-available/coderr (symlinked into sites-enabled/) and routes by prefix:

URL prefix Handled by
/ Static frontend from /var/www/coderr/frontend
/api/ Proxied to Gunicorn on 127.0.0.1:8000
/admin/ Proxied to Gunicorn on 127.0.0.1:8000
/static/ Served from disk (STATIC_ROOT)
/media/ Served from disk (MEDIA_ROOT)

Nginx also performs the HTTP→HTTPS redirect. Django deliberately does not: SECURE_SSL_REDIRECT and the HSTS settings stay unset, because a missing forwarded header would turn them into a redirect loop. This is why manage.py check --deploy still reports security.W004 and security.W008 — both are expected here and are recorded in DEVIATIONS.md.

sudo nginx -t && sudo systemctl reload nginx

Required proxy headers

SECURE_PROXY_SSL_HEADER makes Django trust the forwarded scheme, so the proxied locations must always set it — otherwise DRF builds http:// URLs for uploaded images:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;

Setting the header unconditionally also overwrites any value a client sends, which is what keeps it trustworthy.

Environment variables on the server

The server .env lives at /var/www/coderr/backend/.env and is never committed.

Variable Production value
SECRET_KEY A freshly generated key, never the dev one
DEBUG False
ALLOWED_HOSTS yannick-oetelshoven.developerakademie.org
CSRF_TRUSTED_ORIGINS https://yannick-oetelshoven.developerakademie.org
CORS_ALLOWED_ORIGINS (present but empty)
  • DEBUG=False is what marks the session and CSRF cookies Secure.
  • CSRF_TRUSTED_ORIGINS needs the scheme, and the https:// form, because the browser only ever sees the TLS origin.
  • CORS_ALLOWED_ORIGINS must be present and empty — a single-origin deployment needs no allowed origin at all. Omitting the key entirely is not the same thing: the setting then falls back to the two localhost development origins.

Static and media files

With DEBUG = False Django serves neither static nor media files. collectstatic must run on every release, and Nginx serves both directories directly from disk:

URL prefix Directory on the server Setting
/static/ /var/www/coderr/backend/staticfiles STATIC_ROOT
/media/ /var/www/coderr/backend/media MEDIA_ROOT

Nginx needs read access to both; uploaded media must stay writable for the Gunicorn user.

Deploying a new release

Run in this order, from /var/www/coderr/backend with the virtual environment activated:

cd /var/www/coderr/backend
source env/bin/activate

git pull
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo supervisorctl restart coderr_gunicorn

Only the last step interrupts the service. collectstatic runs before the restart so the new static files are already in place when the new workers come up.

A note on the database

This deployment keeps SQLite, deliberately. It is a course demo with a single application server, one Gunicorn process group and no concurrent write load; SQLite handles that comfortably and keeps the deployment reproducible. PostgreSQL is explicitly out of scope for this project — a real multi-writer workload would be the point at which that decision has to be revisited.

The database file lives at /var/www/coderr/backend/db.sqlite3, is not in the repository, and — like media/ — survives a deploy untouched. Both are worth backing up before a migration.

About

REST API backend for Coderr, a freelancer marketplace. Django and DRF with token auth, 23 documented endpoints for offers, orders, reviews and profiles. Contract-driven: selective pagination, image uploads, role-based permissions for business and customer accounts. 194 tests, 99% coverage.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages