Skip to main content

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 EmailStr for 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:

  1. A secure signup flow: Using UserRegister to capture data and UserCreate to process it.
  2. Data Privacy: Using UserPublic to filter out sensitive fields in API responses.
  3. Role-based Updates: Differentiating between what a user can change (UserUpdateMe) and what an admin can change (UserUpdate).
  4. 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.