Phase 3: API Endpoints & Authentication
RESTful Design & JWT Authentication
In this phase we wire up the public face of TaskFlow: API endpoints that follow REST conventions, secured with JWT tokens and guarded by role-based access control.
RESTful URL Structure
A clean REST API maps resources to URLs and uses HTTP methods to express intent:
| Method | URL Pattern | Purpose | Status Code |
|---|---|---|---|
| POST | /api/v1/projects | Create a project | 201 Created |
| GET | /api/v1/projects | List projects | 200 OK |
| GET | /api/v1/projects/{id} | Get one project | 200 OK |
| PUT | /api/v1/projects/{id} | Update a project | 200 OK |
| DELETE | /api/v1/projects/{id} | Delete a project | 204 No Content |
| POST | /api/v1/projects/{id}/tasks | Create a task in project | 201 Created |
Rules of thumb:
- Use plural nouns for collections (
/projects, not/project). - Nest child resources under parents (
/projects/{id}/tasks). - Version your API (
/api/v1/) so breaking changes never surprise clients. - Return the correct status code — which is a decision, not a lookup:
Which status code does this response deserve?
Did the request succeed?
The two branches people collapse are 401-vs-403 and 403-vs-404. Getting them right is what makes an API debuggable by someone who cannot read your logs.
JWT Authentication Flow
TaskFlow uses JSON Web Tokens (JWT) for stateless authentication:
Register, log in, and use the token
The dashed step is the one that makes JWT stateless — and the one that makes revocation hard.
A JWT has three parts separated by dots: header.payload.signature. The server signs the token with a secret key; on every request, it verifies the signature without hitting the database.
That last sentence is the design's whole appeal, and also its price. Because nothing is looked up, nothing can be revoked. A token stays valid until exp passes, even if you delete the user, change their password, or demote them mid-session. The 30-minute expiry in the code below is not an arbitrary number — it is the size of the window during which a stolen or stale token still works. Short-lived access tokens plus a separately-stored refresh token is how production systems buy revocation back; if you need instant revocation, you need server-side state and JWT alone will not give it to you.
Note also the amber badge: the payload is base64url-encoded, not encrypted. Anyone holding the token can read it. Never put anything in a JWT payload you would not hand to the client in plain JSON.
from datetime import datetime, timedelta, timezone
import jwt
from jwt.exceptions import InvalidTokenError
SECRET_KEY = "your-secret-key" # loaded from environment variable
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
Why PyJWT, and why "that library is dead" is worth checking
TaskFlow uses PyJWT, which is also what FastAPI's own security tutorial uses. You will meet a lot of older tutorials built on python-jose instead, and a lot of advice telling you it is abandoned.
That advice is out of date, and checking it takes about ten seconds. python-jose shipped 3.4.0 in February 2025 and 3.5.0 in May 2025; you can see this yourself on its PyPI release history. Repeating "unmaintained since 2021" in 2026 is simply wrong.
There is still a real reason to know about that 3.4.0 release, and it is more useful than the abandonment story: it fixed CVE-2024-33663, an algorithm-confusion vulnerability affecting versions through 3.3.0. Algorithm confusion is the JWT bug class worth carrying with you: if the verifier lets the token decide which algorithm to verify with, an attacker can hand it a token whose header says HS256, signed with your public RSA key as the HMAC secret — and the verifier accepts it.
The defence is one argument, and it is already in the code above and below:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
algorithms=[...] is not boilerplate. It is the line that says the server decides which algorithm is acceptable, not the token. Never omit it, and never build the list from anything that arrived in the request.
The habit generalises past this one library: before you repeat that a package is dead, open its registry page and look at the dates. "Abandoned" is a claim about the world, and it expires.
Password Hashing with pwdlib + Argon2
FastAPI's current documentation recommends pwdlib with the Argon2 backend instead of the older passlib/bcrypt combination. Argon2 is the winner of the Password Hashing Competition and is resistant to GPU-based brute-force attacks:
from pwdlib import PasswordHash
from pwdlib.hashers.argon2 import Argon2Hasher
password_hash = PasswordHash((Argon2Hasher(),))
hashed = password_hash.hash("user-password") # hash
is_valid = password_hash.verify("user-password", hashed) # verify
FastAPI Dependency Injection for Auth
FastAPI's Depends() system lets you inject the current user into any endpoint. The dependency reads the Authorization: Bearer <token> header, decodes the JWT, looks up the user in the database, and returns the user object (or raises 401 Unauthorized):
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
import jwt
from jwt.exceptions import InvalidTokenError
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
except InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
user = await db.get(User, int(user_id))
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return user
Any endpoint that needs authentication simply adds current_user: User = Depends(get_current_user) to its signature. FastAPI handles the rest.
Role-Based Access Control (RBAC)
In TaskFlow, every user has a role within a project (owner, admin, or member). Access checks happen through another dependency:
from enum import Enum
class ProjectRole(str, Enum):
OWNER = "owner"
ADMIN = "admin"
MEMBER = "member"
def require_project_role(*allowed_roles: ProjectRole):
async def checker(
project_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> ProjectMember:
member = await get_project_member(db, project_id, current_user.id)
if member is None or member.role not in allowed_roles:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return member
return checker
Usage in an endpoint:
@router.put("/projects/{project_id}")
async def update_project(
project_id: int,
data: ProjectUpdate,
member: ProjectMember = Depends(
require_project_role(ProjectRole.OWNER, ProjectRole.ADMIN)
),
db: AsyncSession = Depends(get_db),
):
# only owner or admin reaches this line
...
Pagination Pattern
For list endpoints, TaskFlow uses page/size pagination with a total count so the frontend can render page controls:
from pydantic import BaseModel
from typing import Generic, TypeVar, Sequence
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
items: Sequence[T]
total: int
page: int
size: int
pages: int
Query parameters ?page=1&size=20 drive the offset calculation: offset = (page - 1) * size. The response always includes total (the full count) and pages (the total number of pages).
Automatic OpenAPI Docs
FastAPI generates interactive API documentation from your code at no extra effort:
- Swagger UI at
/docs-- test endpoints directly in the browser. - ReDoc at
/redoc-- a clean, readable reference.
Pydantic v2 models, response status codes, and dependency-injected auth all appear automatically in the generated docs. Clients can export the OpenAPI JSON from /openapi.json to generate SDKs in any language.
Next: Hands-on lab -- you will build all auth, project, and task endpoints for TaskFlow. :::
Sign in to rate