31.2. User Management
Table of Contents
Overview
In an e‑commerce backend, “User Management” is everything related to customers and admins as data in your system, and the operations you perform on them. In this chapter you will not build authentication or authorization, because those are separate chapters. Instead, you will focus on how to model users, store them in the database, and expose clean, RESTful endpoints that the rest of your e‑commerce backend and frontend can rely on.
You will see how user management connects to other parts of the project, such as orders, carts, and admin features, and how to keep the user model simple but flexible.
User Roles in an E‑Commerce Backend
Most e‑commerce applications have at least these user types:
| Role | Description | Typical actions |
|---|---|---|
| Customer | Regular buyer using the storefront | Register, login, browse, add to cart, place orders |
| Admin | Internal user managing the system | Manage products, orders, users, refunds, discounts |
| Support | Optional, handles customer issues | View orders, update order status, assist customers |
| Guest | Not stored as a user, but relevant to flows like cart | Browse, sometimes keep a temporary cart |
You typically represent roles in one of these ways:
- Simple enum column
rolecolumn with values like"customer","admin".- Easy to understand, sufficient for many projects.
- Separate role/permission system
- A
rolestable anduser_rolesjoin table. - More complex, usually combined with an authorization module.
- Better for larger backends with many distinct permissions.
For this project, use a single role column on the users table, for example:
role VARCHAR(20) NOT NULL DEFAULT 'customer'This is enough to distinguish regular customers from admins in the rest of the e‑commerce backend.
Designing the User Model
User management starts with a solid database model for users. The goal is to store necessary information, but not to mix in unrelated concerns such as cart details or order data.
Essential fields
A minimal users table for an e‑commerce backend might look like this:
| Column | Type | Purpose |
|---|---|---|
| id | UUID or BIGSERIAL | Primary key |
| VARCHAR, unique | Login identifier, communication | |
| password_hash | TEXT | Hashed password (never store plain text) |
| first_name | VARCHAR | User’s given name |
| last_name | VARCHAR | User’s family name |
| role | VARCHAR | "customer" or "admin" |
| is_active | BOOLEAN | Can the user log in and use the system |
| is_email_verified | BOOLEAN | Whether email was confirmed |
| created_at | TIMESTAMP WITH TZ | When the user registered |
| updated_at | TIMESTAMP WITH TZ | Last profile update |
You may also store contact and marketing preferences, such as:
| Column | Type | Purpose |
|---|---|---|
| phone | VARCHAR | Optional contact number |
| marketing_opt_in | BOOLEAN | Whether they accept marketing emails |
| last_login_at | TIMESTAMP WITH TZ | Last successful login time |
Always store passwords as secure hashes, never as plain text or reversible encryption. The exact hashing method is covered in the Password Hashing chapter, not here.
Example table definition
A possible PostgreSQL schema for the users table:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'customer',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_email_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone VARCHAR(30),
marketing_opt_in BOOLEAN NOT NULL DEFAULT FALSE,
last_login_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
You will later connect this users table to other parts of the e‑commerce schema, such as orders and addresses, using foreign keys.
Separating Users from Addresses
In many e‑commerce systems, a user may have multiple addresses:
- Shipping addresses
- Billing addresses
- Work vs home
It is tempting to put address columns directly on the users table, such as street, city, country. That restricts the user to a single address and makes it harder to manage multiple addresses.
Instead, keep addresses in their own table and link them to users:
CREATE TABLE user_addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
label VARCHAR(50), -- for example "Home", "Office"
full_name VARCHAR(150) NOT NULL,
line1 VARCHAR(255) NOT NULL,
line2 VARCHAR(255),
city VARCHAR(100) NOT NULL,
state VARCHAR(100),
postal_code VARCHAR(20) NOT NULL,
country_code CHAR(2) NOT NULL, -- ISO 3166-1 alpha-2, for example "US"
phone VARCHAR(30),
is_default_shipping BOOLEAN NOT NULL DEFAULT FALSE,
is_default_billing BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Key points:
user_idconnects the address to theuserstable.ON DELETE CASCADEmeans that if a user is removed, their addresses are also removed.- You support multiple addresses per user, with flags for default shipping and billing.
Example: A single customer with multiple addresses
| user_id | address label | city | default_shipping | default_billing |
|---|---|---|---|---|
| U1 | Home | Berlin | true | true |
| U1 | Office | Berlin | false | false |
| U1 | Parents | Munich | false | false |
During checkout, the cart or order system references one of these address records.
Public vs Internal User Data
Your API will often need to return user data, for example:
- A customer viewing their own profile.
- An admin browsing a paginated list of users.
- The frontend showing the current user name in the navigation bar.
However, you must never expose sensitive fields directly, such as:
password_hash- Internal flags that users should not see, for example
is_admin_overrideor internal notes.
The solution is to define separate models for:
- Data stored in your database.
- Data accepted in requests.
- Data returned in responses.
Example with FastAPI and Pydantic-style models:
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
class UserBase(BaseModel):
email: EmailStr
first_name: str
last_name: str
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
first_name: Optional[str] = None
last_name: Optional[str] = None
phone: Optional[str] = None
marketing_opt_in: Optional[bool] = None
class UserPublic(UserBase):
id: str
role: str
is_active: bool
is_email_verified: bool
created_at: datetimeNotice:
UserCreateincludes apassword, butUserPublicdoes not.- The database model (for example SQLAlchemy
Userclass) will containpassword_hash, but the public model will not expose it.
Never return password or password_hash fields in any API response, even to the user who owns the account.
User‑Related Endpoints in the E‑Commerce API
User management in this e‑commerce project focuses on operations like:
- Creating a new user record when a user registers.
- Reading a user profile.
- Updating basic user details.
- Managing addresses.
- Admin operations over users.
Authentication and token handling are covered in separate chapters. Here you should assume that:
- There is a way to identify the currently authenticated user from the request.
- Admin endpoints are protected by an authorization layer that checks user roles.
Typical customer‑facing user endpoints
Here is a set of common REST endpoints for customers:
| Method | Path | Description |
|---|---|---|
| POST | /users | Create a new user (registration) |
| GET | /users/me | Get current user profile |
| PATCH | /users/me | Update current user profile |
| GET | /users/me/addresses | List current user addresses |
| POST | /users/me/addresses | Add new address |
| PATCH | /users/me/addresses/{id} | Update address |
| DELETE | /users/me/addresses/{id} | Remove address |
| PATCH | /users/me/addresses/{id}/default-shipping | Set default shipping address |
The exact paths can vary, but it is good practice to:
- Keep all user‑self operations under
/users/me. - Avoid exposing user IDs to customers unless necessary.
Example: Create user (registration) endpoint
Even though the Registration logic like email verification is in another chapter, you still define which fields go into the user:
Request:
POST /users
{
"email": "alice@example.com",
"password": "strong_password_123",
"first_name": "Alice",
"last_name": "Doe",
"marketing_opt_in": true
}Backend actions:
- Validate the input.
- Check that
emailis not already in use. - Hash the password and store it in
password_hash. - Insert a new row into
users. - Return a
UserPublicrepresentation.
Response:
201 Created
{
"id": "a3a737da-63bc-4c5b-95e9-b755c6715e86",
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Doe",
"role": "customer",
"is_active": true,
"is_email_verified": false,
"created_at": "2026-03-10T12:34:56Z"
}Example: Get current user profile
Assuming a token is already checked by authentication middleware, your endpoint might look like this:
GET /users/me
Authorization: Bearer <token>Response:
200 OK
{
"id": "a3a737da-63bc-4c5b-95e9-b755c6715e86",
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Doe",
"role": "customer",
"is_active": true,
"is_email_verified": true,
"created_at": "2026-03-10T12:34:56Z"
}The backend finds the user by the ID extracted from the token and returns the public representation.
Example: Update current user profile
A partial update is usually done through PATCH:
PATCH /users/me
Content-Type: application/json
Authorization: Bearer <token>
{
"first_name": "Alicia",
"marketing_opt_in": false
}Your backend:
- Loads the current user.
- Applies only the fields present in the body.
- Saves the updated record.
- Returns the updated public user.
Response:
200 OK
{
"id": "a3a737da-63bc-4c5b-95e9-b755c6715e86",
"email": "alice@example.com",
"first_name": "Alicia",
"last_name": "Doe",
"role": "customer",
"is_active": true,
"is_email_verified": true,
"created_at": "2026-03-10T12:34:56Z"
}Admin user endpoints
Admins need broader access to the user base, for example:
| Method | Path | Description |
|---|---|---|
| GET | /admin/users | List users, with pagination |
| GET | /admin/users/{id} | View a specific user |
| PATCH | /admin/users/{id} | Update user role or status |
| DELETE | /admin/users/{id} | Deactivate or remove a user |
These endpoints should be protected so that only admins can access them. The implementation of that protection is covered in the Authorization chapter.
Example: Admin list users
GET /admin/users?page=1&limit=20&email=alice@example.com
Authorization: Bearer <admin-token>Possible response:
200 OK
{
"items": [
{
"id": "a3a737da-63bc-4c5b-95e9-b755c6715e86",
"email": "alice@example.com",
"first_name": "Alicia",
"last_name": "Doe",
"role": "customer",
"is_active": true,
"is_email_verified": true,
"created_at": "2026-03-10T12:34:56Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}This shows how user management integrates with pagination and filtering features of your API.
Relationship between Users, Carts, and Orders
In an e‑commerce backend, users are central to other entities. You will rely on the users table as a foreign key target for several features:
| Related entity | Relationship to users | Example column |
|---|---|---|
| Carts | One open cart per user (usually) | carts.user_id |
| Orders | Many orders per user | orders.user_id |
| Addresses | Many addresses per user | user_addresses.user_id |
| Payment methods | Many per user (if you store them) | payment_methods.user_id |
| Reviews | Many reviews per user | product_reviews.user_id |
Even if you allow guest checkout, it is often convenient to create a “shadow” user record for each guest order. That way:
- You can send order confirmations.
- If the guest later creates a full account, you can link it to their previous orders.
For example:
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
total_amount NUMERIC(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL,
placed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Each order belongs to exactly one user. In application code, you might fetch all orders for the current user like this (pseudocode):
def get_my_orders(current_user_id: str):
return db.query(Order).filter(Order.user_id == current_user_id).all()User management must be designed in a way that these relationships are straightforward and efficient.
Example Implementation Sketch with FastAPI and SQLAlchemy
Below is a simplified sketch, showing how user management might look for this project. It focuses on structure, not on every detail.
SQLAlchemy models
from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import uuid
from datetime import datetime
from .database import Base # your Base from SQLAlchemy setup
class User(Base):
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String(255), unique=True, nullable=False)
password_hash = Column(String, nullable=False)
first_name = Column(String(100), nullable=False)
last_name = Column(String(100), nullable=False)
role = Column(String(20), nullable=False, default="customer")
is_active = Column(Boolean, nullable=False, default=True)
is_email_verified = Column(Boolean, nullable=False, default=False)
phone = Column(String(30))
marketing_opt_in = Column(Boolean, nullable=False, default=False)
last_login_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, nullable=False)
addresses = relationship("UserAddress", back_populates="user", cascade="all, delete-orphan")
class UserAddress(Base):
__tablename__ = "user_addresses"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
label = Column(String(50))
full_name = Column(String(150), nullable=False)
line1 = Column(String(255), nullable=False)
line2 = Column(String(255))
city = Column(String(100), nullable=False)
state = Column(String(100))
postal_code = Column(String(20), nullable=False)
country_code = Column(String(2), nullable=False)
phone = Column(String(30))
is_default_shipping = Column(Boolean, nullable=False, default=False)
is_default_billing = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, nullable=False)
user = relationship("User", back_populates="addresses")Router example for `/users/me`
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from . import models, schemas
from .dependencies import get_db, get_current_user # defined in your auth-related code
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/me", response_model=schemas.UserPublic)
def read_current_user(
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
return current_user
@router.patch("/me", response_model=schemas.UserPublic)
def update_current_user(
payload: schemas.UserUpdate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
if payload.first_name is not None:
current_user.first_name = payload.first_name
if payload.last_name is not None:
current_user.last_name = payload.last_name
if payload.phone is not None:
current_user.phone = payload.phone
if payload.marketing_opt_in is not None:
current_user.marketing_opt_in = payload.marketing_opt_in
db.add(current_user)
db.commit()
db.refresh(current_user)
return current_user
The get_current_user dependency and password hashing routines are not part of this chapter. They are handled by authentication components of your backend.
Soft Deletion vs Hard Deletion
User records are often tied to orders, invoices, and legal requirements, so deleting them fully can be problematic. Instead, many systems use soft deletion, where the user is marked as inactive while their data stays in the database.
Common approaches:
| Strategy | How it works | Example column |
|---|---|---|
is_active flag | Prevents login and interactions | is_active = false |
deleted_at column | Timestamp of deactivation | deleted_at timestamp |
| Full deletion | Remove record and cascade to related tables | DELETE FROM users |
For an e‑commerce system, a common pattern is:
- Use
is_active = falseto disable accounts. - Keep orders and related data intact.
- Optionally anonymize personal fields, for example replace
emailwith a hashed value.
Example admin action:
def deactivate_user(user: User, db: Session):
user.is_active = False
db.add(user)
db.commit()Your authentication layer should prevent inactive users from logging in or using the API.
Summary
In this chapter you saw how user management fits into the e‑commerce backend project:
- Define a clean
userstable with roles and status flags. - Separate user data from addresses, so each user can have multiple addresses.
- Expose safe public representations of users, and never return password data.
- Provide user‑self endpoints under
/users/meand reserved admin endpoints under/admin/users. - Link users to carts, orders, and other e‑commerce entities using foreign keys.
- Prefer soft deletion or deactivation for users, instead of removing them entirely.
These foundations allow the rest of the e‑commerce backend, such as orders, carts, and admin tools, to rely on a robust and consistent user system.
Views: 10
KAHIBARO