Skip to main content
← All projects
L2Backend API· 8-15h total

Multi-user TODO API with JWT auth

This is the most common junior backend interview project on the planet: REST API, auth, persistence, tests. Builds the muscle hiring managers screen for first.

Open in GitHub Codespaces· free 60h/moOpen in Gitpod

Resume bullet (when finished)

Built a multi-user REST API with JWT auth, rate-limiting, and PostgreSQL backed by 65% test coverage and a Docker deploy.

Locked tech stack

No "choose your language" — analysis paralysis kills completion. Follow the stack to the letter on your first build.

Python 3.12FastAPIPostgreSQLSQLAlchemy 2.0Pydantic v2JWT (PyJWT)pytest

Milestones (7 · ~15h)

  1. M1~1h

    Scaffold + /health endpoint

    FastAPI server runs on :8000; GET /health returns 200 with {status: 'ok'}.

    CHECK BEFORE MOVING ON:

    • What does uvicorn do that FastAPI itself doesn't?
    • Why is /health a separate endpoint from /?
    $ git commit -m "feat: scaffold project + /health endpoint"
  2. M2~2h

    User registration + bcrypt hashing

    POST /register accepts {email, password}, persists user with password hashed via bcrypt. Returns 201 on success, 409 on duplicate email.

    CHECK BEFORE MOVING ON:

    • Why bcrypt over SHA-256 for passwords?
    • What's the cost factor and why would you bump it in 2026?
    $ git commit -m "feat(auth): user registration with bcrypt password hashing"
  3. M3~2h

    JWT login + protected route

    POST /login returns a JWT bearer token. GET /me requires it; returns the user. Wrong token returns 401.

    CHECK BEFORE MOVING ON:

    • What should NEVER be inside a JWT payload?
    • What's the difference between access tokens and refresh tokens?
    $ git commit -m "feat(auth): JWT login + protected /me endpoint"
  4. M4~3h

    TODO CRUD per user

    GET/POST/PATCH/DELETE /todos — each user only sees their own. Pagination with ?limit + ?offset.

    CHECK BEFORE MOVING ON:

    • How do you enforce 'user only sees their own todos' — at the SQL layer or in Python?
    • What's wrong with PATCH that accepts arbitrary JSON?
    $ git commit -m "feat(todos): CRUD scoped to the authenticated user"
  5. M5~2h

    Rate limiting on /register and /login

    Limit /register to 5/min/IP and /login to 10/min/IP. Use slowapi or roll your own with Redis.

    CHECK BEFORE MOVING ON:

    • Why do auth endpoints need stricter limits than CRUD?
    • What's a good 429 response body shape?
    $ git commit -m "feat: rate-limit auth endpoints"
  6. M6~3h

    Tests — 65% coverage minimum

    pytest + httpx async client. Cover auth happy + sad paths, CRUD per-user isolation, rate-limit trigger.

    CHECK BEFORE MOVING ON:

    • What's the difference between a unit and an integration test in a FastAPI project?
    • How do you test rate-limiting without sleeping in the test?
    $ git commit -m "test: 65% coverage across auth + todos"
  7. M7~2h

    Docker + README

    Multi-stage Dockerfile. README covers: setup, first request, schema diagram, design decisions.

    CHECK BEFORE MOVING ON:

    • Why multi-stage builds vs one big image?
    • What's the smallest practical base image for production Python in 2026?
    $ git commit -m "ops: multi-stage Dockerfile + production README"

60-second demo storyboard

What you say in the recruiter screen when they ask "tell me about your latest project." Practice it out loud.

  1. 0-5s: 'I built a multi-user TODO API with auth, rate-limiting, and Docker.'
  2. 5-15s: Live demo — POST /register, POST /login, see the JWT come back, paste it as bearer token.
  3. 15-30s: GET /todos — only your own. Try with a stolen token from another user → 403.
  4. 30-45s: Show the rate-limiter — six rapid /login attempts → 429 with Retry-After.
  5. 45-55s: One design decision in plain English (e.g. 'I chose RLS at the SQL layer instead of Python filtering because…').
  6. 55-60s: 'Code on GitHub, deployed at <url>. Would love your feedback on the auth design.'

STAR talking points for behavioral round

STAR — DEBUGGING CHALLENGE

Situation: tests passed locally but failed in CI. Task: find why. Action: discovered bcrypt's cost factor was different across environments via an env-var quirk. Result: added a guard in conftest.py pinning cost to 4 in tests, full test suite back to green, also documented the env-var.

STAR — ARCHITECTURE TRADE-OFF

Situation: had to choose between Python filtering and Postgres RLS for the 'user sees only their todos' guarantee. Task: justify the choice. Action: chose RLS — single source of truth, can't accidentally bypass with a missed WHERE. Result: zero authorization bugs in the test suite, even when I deliberately tried to leak data with a mis-scoped query.

STAR — SHIPPED UNDER DEADLINE

Situation: needed to ship the project before applying to roles. Task: scope down without losing portfolio signal. Action: cut websockets, kept REST + auth + rate-limiting + tests + Docker. Result: shipped in 12 hours instead of 25, still hit all four resume bullets that matter for junior backend.

Production references — how grown-up systems do this

Stripe

Stripe's REST API uses a similar bearer-token + idempotency-key pattern; their docs are the gold standard.

Supabase

Supabase Row Level Security is the easiest production-grade way to enforce 'user only sees their rows' — same idea, Postgres-native.

GitHub

GitHub's REST API uses scope-based token permissions — the next step up from a single JWT once your app has more than one role.

Self-review rubric (before you claim done)

Correctness

  • All endpoints match the OpenAPI spec (auto-generated via FastAPI).
  • Auth: wrong token returns 401, missing token returns 401, expired token returns 401.
  • CRUD: user can only see + edit their own todos. Cross-user attempts return 403 or 404 (not 200).
  • Rate limiter: 6th rapid /login from same IP returns 429 with Retry-After.

Code quality

  • No N+1 queries (verify with SQL log on /todos list).
  • Type hints on every function signature, including async ones.
  • Pydantic models for every request + response — no naked dicts.
  • Secrets via env vars only — never committed.

Testing

  • ≥65% line coverage (pytest --cov).
  • At least one integration test that hits the real DB.
  • Auth happy + sad paths both covered.
  • Rate-limiter test does not sleep — uses dependency override.

Docs

  • README has: setup, first API call, schema diagram, three design decisions written in plain English.
  • .env.example checked in, .env in .gitignore.
  • OpenAPI URL linked in README so reviewers can poke around at /docs.

✱ AI code review

Get a senior-style review before you call it done

Push your finished work to GitHub, open a PR, paste the PR URL below. Claude reviews the diff against this project's rubric and replies with strengths, must-fix items, and one teachable principle.

Tick the rubric items honestly, write the README, push to GitHub, get the AI review above. Once it's clean, email support@learnpython.academy with the repo link — we feature the best ones on /success-stories.

Need Python first? Start Foundations →