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.