KAHIBARO
Discord Login Register

31.2. User Management

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:

RoleDescriptionTypical actions
CustomerRegular buyer using the storefrontRegister, login, browse, add to cart, place orders
AdminInternal user managing the systemManage products, orders, users, refunds, discounts
SupportOptional, handles customer issuesView orders, update order status, assist customers
GuestNot stored as a user, but relevant to flows like cartBrowse, sometimes keep a temporary cart

You typically represent roles in one of these ways:

  1. Simple enum column
    • role column with values like "customer", "admin".
    • Easy to understand, sufficient for many projects.
  2. Separate role/permission system
    • A roles table and user_roles join 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:

sql
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:

ColumnTypePurpose
idUUID or BIGSERIALPrimary key
emailVARCHAR, uniqueLogin identifier, communication
password_hashTEXTHashed password (never store plain text)
first_nameVARCHARUser’s given name
last_nameVARCHARUser’s family name
roleVARCHAR"customer" or "admin"
is_activeBOOLEANCan the user log in and use the system
is_email_verifiedBOOLEANWhether email was confirmed
created_atTIMESTAMP WITH TZWhen the user registered
updated_atTIMESTAMP WITH TZLast profile update

You may also store contact and marketing preferences, such as:

ColumnTypePurpose
phoneVARCHAROptional contact number
marketing_opt_inBOOLEANWhether they accept marketing emails
last_login_atTIMESTAMP WITH TZLast 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:

sql
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:

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:

sql
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:

Example: A single customer with multiple addresses

user_idaddress labelcitydefault_shippingdefault_billing
U1HomeBerlintruetrue
U1OfficeBerlinfalsefalse
U1ParentsMunichfalsefalse

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:

However, you must never expose sensitive fields directly, such as:

The solution is to define separate models for:

Example with FastAPI and Pydantic-style models:

python
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: datetime

Notice:

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:

Authentication and token handling are covered in separate chapters. Here you should assume that:

Typical customer‑facing user endpoints

Here is a set of common REST endpoints for customers:

MethodPathDescription
POST/usersCreate a new user (registration)
GET/users/meGet current user profile
PATCH/users/meUpdate current user profile
GET/users/me/addressesList current user addresses
POST/users/me/addressesAdd new address
PATCH/users/me/addresses/{id}Update address
DELETE/users/me/addresses/{id}Remove address
PATCH/users/me/addresses/{id}/default-shippingSet default shipping address

The exact paths can vary, but it is good practice to:

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:

json
POST /users
{
  "email": "alice@example.com",
  "password": "strong_password_123",
  "first_name": "Alice",
  "last_name": "Doe",
  "marketing_opt_in": true
}

Backend actions:

  1. Validate the input.
  2. Check that email is not already in use.
  3. Hash the password and store it in password_hash.
  4. Insert a new row into users.
  5. Return a UserPublic representation.

Response:

json
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:

http
GET /users/me
Authorization: Bearer <token>

Response:

json
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:

http
PATCH /users/me
Content-Type: application/json
Authorization: Bearer <token>
{
  "first_name": "Alicia",
  "marketing_opt_in": false
}

Your backend:

  1. Loads the current user.
  2. Applies only the fields present in the body.
  3. Saves the updated record.
  4. Returns the updated public user.

Response:

json
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:

MethodPathDescription
GET/admin/usersList 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

http
GET /admin/users?page=1&limit=20&email=alice@example.com
Authorization: Bearer <admin-token>

Possible response:

json
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 entityRelationship to usersExample column
CartsOne open cart per user (usually)carts.user_id
OrdersMany orders per userorders.user_id
AddressesMany addresses per useruser_addresses.user_id
Payment methodsMany per user (if you store them)payment_methods.user_id
ReviewsMany reviews per userproduct_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:

For example:

sql
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):

python
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

python
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`

python
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:

StrategyHow it worksExample column
is_active flagPrevents login and interactionsis_active = false
deleted_at columnTimestamp of deactivationdeleted_at timestamp
Full deletionRemove record and cascade to related tablesDELETE FROM users

For an e‑commerce system, a common pattern is:

Example admin action:

python
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:

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

Comments

Please login to add a comment.

Don't have an account? Register now!