Implementing User Registration and Profiles
In this tutorial, you will implement a complete user management system that handles public registration, administrative user creation, and profile updates. You will use a layered approach with specialized SQLModel schemas to ensure data privacy and strict validation at every stage of the user lifecycle.
Prerequisites
To follow this tutorial, you need to be familiar with:
- SQLModel: For defining database models and API schemas.
- FastAPI: For handling HTTP requests and dependency injection.
- Pydantic: Specifically
EmailStrfor email validation.
Step 1: Define the Data Foundation
The foundation of user management in this project is the UserBase class. This model defines the core fields shared across registration, updates, and public profiles.
In backend/app/models.py, define the base schema:
from sqlmodel import Field, SQLModel
from pydantic import EmailStr
class UserBase(SQLModel):
email: EmailStr = Field(unique=True, index=True, max_length=255)
is_active: bool = True
is_superuser: bool = False
full_name: str | None = Field(default=None, max_length=255)
By using UserBase, you ensure that every user-related model consistently enforces email uniqueness and maximum field lengths.
Step 2: Implement Public Signup
When a new user signs up, you need to collect their email and password without exposing internal fields like is_superuser. You will use UserRegister for the initial request and UserCreate for the internal database operation.
In backend/app/models.py:
class UserRegister(SQLModel):
email: EmailStr = Field(max_length=255)
password: str = Field(min_length=8, max_length=128)
full_name: str | None = Field(default=None, max_length=255)
class UserCreate(UserBase):
password: str = Field(min_length=8, max_length=128)
In your API route (backend/app/api/routes/users.py), you map the public registration data to the internal creation model:
@router.post("/signup", response_model=UserPublic)
def register_user(session: SessionDep, user_in: UserRegister) -> Any:
"""
Create new user without the need to be logged in.
"""
user = crud.get_user_by_email(session=session, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system",
)
# Convert UserRegister to UserCreate
user_create = UserCreate.model_validate(user_in)
user = crud.create_user(session=session, user_create=user_create)
return user
This separation prevents users from self-assigning superuser status during registration.
Step 3: Control Data Exposure
To prevent leaking sensitive information like hashed passwords, you must define schemas for data egress. UserPublic provides the safe view of a single user, while UsersPublic handles paginated lists.
In backend/app/models.py:
import uuid
from datetime import datetime
class UserPublic(UserBase):
id: uuid.UUID
created_at: datetime | None = None
class UsersPublic(SQLModel):
data: list[UserPublic]
count: int
Always use UserPublic as the response_model in your FastAPI decorators to ensure that only the fields defined in UserBase plus the id and created_at timestamp are returned to the client.
Step 4: Enable Profile Updates
Users and administrators have different requirements for updating profiles. Users should only change their own basic info, while admins might need to change roles or passwords.
User Self-Updates
Use UserUpdateMe to restrict users to updating only their full_name and email.
In backend/app/models.py:
class UserUpdateMe(SQLModel):
full_name: str | None = Field(default=None, max_length=255)
email: EmailStr | None = Field(default=None, max_length=255)
In backend/app/api/routes/users.py, implement the update logic with a check for email collisions:
@router.patch("/me", response_model=UserPublic)
def update_user_me(*, session: SessionDep, user_in: UserUpdateMe, current_user: CurrentUser) -> Any:
if user_in.email:
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
if existing_user and existing_user.id != current_user.id:
raise HTTPException(status_code=409, detail="User with this email already exists")
user_data = user_in.model_dump(exclude_unset=True)
current_user.sqlmodel_update(user_data)
session.add(current_user)
session.commit()
session.refresh(current_user)
return current_user
Administrative Updates
Use UserUpdate for superusers. This model inherits from UserBase but makes all fields optional, including the password.
In backend/app/models.py:
class UserUpdate(UserBase):
email: EmailStr | None = Field(default=None, max_length=255) # type: ignore[assignment]
password: str | None = Field(default=None, min_length=8, max_length=128)
The type: ignore[assignment] is necessary because UserUpdate overrides the required email field from UserBase to make it optional for partial updates.
Summary of Results
By following this structure, you have built:
- A secure signup flow: Using
UserRegisterto capture data andUserCreateto process it. - Data Privacy: Using
UserPublicto filter out sensitive fields in API responses. - Role-based Updates: Differentiating between what a user can change (
UserUpdateMe) and what an admin can change (UserUpdate). - Validation: Enforcing 8-character minimum passwords and valid email formats across all entry points.
Next, you can explore backend/app/crud.py to see how these models are used to interact with the database using SQLModel sessions.