Project: Todo API With Authentication

Introduction

This project adds users, JWT login, and personal todo lists—each user sees only their tasks. You practice 401 vs 403, unified error JSON, and protected APIRouter routes. It is the full-stack API capstone before deployment and optional frontend integration.

Prerequisites

Project Goals

  • POST /api/v1/auth/register and POST /api/v1/auth/login
  • GET/POST /api/v1/todos/ — list and create (JWT required)
  • PATCH/DELETE /api/v1/todos/{id} — owner only
  • Error body: {"detail": "..."} or team envelope
  • Optional CORS for http://localhost:5173

Step 1: Models

User: id, username, hashed_password

Todo: id, title, done, owner_idForeignKey("users.id")

Alembic:

bash
alembic revision --autogenerate -m "Users and todos"
alembic upgrade head

Step 2: Auth Routes

Implement from authentication chapter:

  • Register with UserCreate schema
  • Login with OAuth2PasswordRequestForm
  • GET /api/v1/auth/me with Depends(get_current_user)

Step 3: Todo Routes

Ownership check helper:

python
def get_owned_todo(todo_id: int, db: Session, user: User) -> Todo:
    todo = db.get(Todo, todo_id)
    if todo is None:
        raise HTTPException(status_code=404, detail="Todo not found")
    if todo.owner_id != user.id:
        raise HTTPException(status_code=403, detail="Not allowed")
    return todo

Step 4: Unified Errors (Optional)

python
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={"code": exc.status_code, "message": exc.detail, "data": None},
    )

Step 5: Test Flow With curl

Step 6: pytest

python
def test_todos_require_auth(client):
    assert client.get("/api/v1/todos/").status_code == 401
 
def test_user_isolation(client):
    # register user A and B, ensure A cannot delete B's todo
    ...

Use login helper fixture from testing chapter.

Step 7: CORS for Frontend (Optional)

python
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["Authorization", "Content-Type"],
)

Frontend fetch with Authorization: Bearer ${token}.

Compare Flask Todo project—server-rendered vs API-only.

Verification Checklist

  • Register → login → create todo → list shows one item
  • Missing token returns 401
  • Access other user's todo returns 403 or 404 (your policy)
  • pytest covers auth and CRUD
  • Password never in JSON responses

FAQ

Login 422 with JSON body?

OAuth2 login expects form fields—use -d username=...&password=....

Refresh tokens?

Optional stretch—short access token sufficient for learning.

Admin sees all todos?

Add role == admin bypass in get_owned_todo.

OpenAPI Authorize?

Click Authorize in /docs, use login form or paste Bearer token.

Vue/React starter?

Any SPA calling /api/v1 with CORS—backend focus here.

Next project?

Inference API or deploy.