Exercise: Build a Registration Endpoint
You'll:
- implement
POST /auth/registerin the auth router - write two tests: one that registers a user, one that registers and then logs in
- generate a development
JWT_SECRET_KEYand add it to.env - generate and apply the migration that creates the
userstable - register a new user through the docs UI and confirm the full flow works
The auth flow has three steps:
- A user registers with an email and password. The server hashes the password and stores the user.
- The user logs in via
POST /auth/token. The server verifies the password and returns a signed JWT (JSON Web Token). - The client sends that JWT in the
Authorizationheader on every subsequent request. Protected endpoints verify the token usingJWT_SECRET_KEYto identify the caller.
The login endpoint, the security helpers (verify_password, create_access_token, get_current_user), the User model, CurrentUserDep, the crud.create_user helper, and the protected project/task endpoints are all in place on this chapter's branch. You build the registration route and its tests.
Before You Start
Set up the starting state for this exercise:
git fetch
git checkout -b my-ch10 --no-track origin/ch10
uv sync
Files that changed in this chapter:
pyproject.toml(modified, addspwdlib[argon2],pyjwt, andemail-validator)src/release_tracker/config.py(modified, adds thejwt_secret_keysetting)src/release_tracker/models.py(modified, addsUser,UserBase,UserCreate,UserRead,AccessToken)src/release_tracker/crud.py(modified, addsget_user_by_emailandcreate_user)src/release_tracker/security.py(new, password hashing, JWT creation, token validation)src/release_tracker/dependencies.py(modified, addsCurrentUserDep)src/release_tracker/routers/auth.py(modified, login and/meare provided; you'll add/register)src/release_tracker/routers/projects.py(modified, write routes requireCurrentUserDep)src/release_tracker/routers/tasks.py(modified, write routes requireCurrentUserDep)src/release_tracker/main.py(modified, includes the auth router)tests/conftest.py(modified, setsJWT_SECRET_KEYfor tests, addsmake_user,auth_headers,auth_clientfixtures)tests/test_auth.py(modified, 7 login/me tests provided, 2 registration tests stubbed)scripts/seed.py(modified, also seeds a demo user)
Step 1: Generate a JWT_SECRET_KEY
The new jwt_secret_key setting has min_length=32 and no default. Without one in the environment, the application won't start.
Generate a strong key:
uv run python -c "import secrets; print(secrets.token_urlsafe(32))"
Add it to your .env:
DATABASE_URL=postgresql+psycopg://release_tracker:release_tracker@localhost:5432/release_tracker
DEBUG=true
JWT_SECRET_KEY=<paste the generated value here>
Confirm .env is in .gitignore before going further.
Step 2: Generate and Apply the Migration
The new User model needs a users table. Generate the migration and apply it:
uv run alembic revision --autogenerate -m "add users table"
uv run alembic upgrade head
alembic current should print the revision ID for add_users_table.
Step 3: Implement the Register Endpoint
Open src/release_tracker/routers/auth.py.
The login endpoint and /me are provided. At the bottom, a register function is stubbed:
@router.post(
"/register",
response_model=models.UserRead,
status_code=status.HTTP_201_CREATED,
)
def register(payload: models.UserCreate, session: SessionDep) -> Any:
# TODO: Create a new user account.
#
# Use crud.create_user() with the email and password from the payload.
# Return the created user.
#
# Don't worry about duplicate emails — the IntegrityError handler
# from main.py catches that and returns a 409.
raise NotImplementedError
Look at how crud.create_user is called in tests/conftest.py (the make_user fixture) if you want to see what arguments it expects.
Step 4: Write the Registration Tests
Open tests/test_auth.py. The 7 login and /me tests are provided. Two test functions at the bottom are stubbed:
def test_register_creates_user(client: TestClient) -> None:
# TODO: Register a new user via POST /auth/register with a JSON body
# containing "email" and "password".
#
# Assert:
# - status code is 201
# - response body contains the email you sent
# - response body has is_active == True
# - hashed_password is NOT in the response
pass
def test_register_then_login(client: TestClient) -> None:
# TODO: Register a new user, then log in with the same credentials
# via POST /auth/token (form data, not JSON).
#
# Assert:
# - the login response has status 200
# - the response contains an access_token
pass
Remember: /auth/register accepts JSON, but /auth/token accepts form data (data={}, not json={}).
Step 5: Run the Tests
uv run pytest
All tests should pass: the project tests, the task tests, the 7 provided auth tests, plus the 2 you wrote.
Step 6: Try It in /docs
Reseed the database (the seed script creates a demo user) and start the server:
uv run python -m scripts.seed
uv run fastapi dev
Open http://localhost:8000/docs, then:
- Try
POST /auth/registerwith a new email and password. Expect a201with the user's email and ID. - Try registering the same email again. Expect a
409(the IntegrityError handler catches the duplicate). - Click Authorize at the top of the page. Enter the email and password you just registered with. Click Authorize, then Close.
- Try
POST /projects/with a valid body. Expect a201. - Click the lock icon to log out, then retry the same
POST. Expect a401.
Success
If pytest is green and you can register, log in, and hit protected endpoints from the docs UI, the registration endpoint is wired correctly and the full auth flow works end-to-end.
Troubleshooting Tips
-
Server won't start; Pydantic complains about
JWT_SECRET_KEY:The setting has
min_length=32and no default. The.enveither doesn't exist, isn't being loaded (wrong working directory), or has a key shorter than 32 characters. -
POST /auth/registerreturns 500 instead of 201:Check that
crud.create_useris being called with keyword arguments (email=andpassword=). Positional arguments in the wrong order will pass the email as the password. -
POST /auth/registerreturns 422:The request body must be JSON (
{"email": "...", "password": "..."}), not form data. Unlike/auth/token, the register endpoint uses a Pydantic model, notOAuth2PasswordRequestForm. -
Duplicate registration returns 500 instead of 409:
The
IntegrityErrorhandler inmain.pyshould already be in place from the chapter branch. Confirmmain.pyhas the exception handler that catchesIntegrityErrorand returns a 409. -
Seed script fails with
UniqueViolationorUndefinedTable:The database carries forward across branch switches, so data from earlier chapters can conflict with the seed script. Reset to a clean database and re-run:
shelldocker compose down -v && docker compose up -d uv run alembic upgrade head uv run python -m scripts.seedThe
-vflag deletes the Postgres data volume, wiping everything. This is fine for local development but never appropriate for a shared or production database.
Compare with the Solution
git diff solutions/ch10 -- src/ tests/