FastAPI - Beginner to Pro / Authentication & Authorization

Create First User

🛠 Enhancing auth.py — Creating a Real User Endpoint

In this lesson, we move from a placeholder authentication route to building the foundation of a real user creation system.

Up until now, our routers/auth.py looked like this:

from fastapi import APIRouter

router = APIRouter()

@router.get("/auth/")
async def get_user():
    return {"user": "authenticated"}

This endpoint simply returned static data.

Now we’re going to build something meaningful.


1️⃣ Update Existing endpoint

Creating a resource in a REST API must use POST, not GET.

So we change this:

🗂️ routers/auth.py

@router.get("/auth/")

To this:

🗂️ routers/auth.py

@router.post("/auth/")

And we rename the function to reflect its responsibility:

🗂️ routers/auth.py

@router.post("/auth/")
async def create_user():
    return {"user": "authenticated"}

We are now creating a user → therefore we use POST.


2️⃣ Introduce Request Validation

Before writing any logic, we define what data we expect from the client.

In backend development:

Never trust raw input.

We use Pydantic to validate incoming request data.

🗂️ routers/auth.py

from fastapi import APIRouter
from pydantic import BaseModel

router = APIRouter()

class CreateUserRequest(BaseModel):
    username: str
    email: str
    first_name: str
    last_name: str
    password: str
    role: str

3️⃣ Accept the Request Object

Now we pass this model into our route:

🗂️ routers/auth.py

@router.post("/auth/")
async def create_user(create_user_request: CreateUserRequest):
    return {"message": "User received"}

FastAPI now:

  1. Receives JSON

  2. Validates it

  3. Converts it into a CreateUserRequest object

  4. Injects it into the function

This is dependency injection and data parsing handled automatically.


4️⃣ Connect to Our Database Model

Inside models.py, we previously defined:

🗂️ models.py

class Users(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True)
    username = Column(String, unique=True)
    first_name = Column(String)
    last_name = Column(String)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)
    role = Column(String)

Notice something critical:

Our reuest model has:

password
class CreateUserRequest(BaseModel):
    username: str
    email: str
    first_name: str
    last_name: str
    password: str
    role: str

But our database model has:

hashed_password
class Users(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True)
    username = Column(String, unique=True)
    first_name = Column(String)
    last_name = Column(String)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)
    role = Column(String)

This is intentional.

We never store plain passwords in a database.


5️⃣ Why *model_dump() Will Fail

You might try:

create_user_model = Users(**create_user_request.model_dump())

This will fail.

Why?

Because:

  • CreateUserRequest contains password

  • Users expects hashed_password

They do not match.

This teaches an important backend concept:

API schemas and ORM models should not always be identical.

They represent different layers of the application.


6️⃣ Manual Field Mapping (Transformation Layer)

Instead, we map fields explicitly:

🗂️ routers/auth.py

from models import Users

@router.post("/auth/")
async def create_user(create_user_request: CreateUserRequest):

    create_user_model = Users(
        email=create_user_request.email,
        username=create_user_request.username,
        first_name=create_user_request.first_name,
        last_name=create_user_request.last_name,
        role=create_user_request.role,
        hashed_password=create_user_request.password,  # temporary
        is_active=True
    )

    return create_user_model

Important:

  • We are NOT hashing yet

  • We are NOT saving to the database yet

  • We are only creating an ORM instance

This creates a SQLAlchemy object in memory.

It does NOT persist anything.

Understanding that distinction is critical in backend development.


7️⃣ Testing the Endpoint

Run your application:

terminal

uvicorn main:app --reload

Go to:

/docs

You will now see:

  • POST /auth/

Send demo data.

You’ll receive:

  • Status code: 200

  • User object returned

This proves:

  • Validation works

  • Object creation works

  • Field mapping works

But we are not production-ready yet.


🔐 Important Security Note

Right now, we are saving passwords as plain text.

That is unsafe.

In the next section, we will:

  • Hash passwords

  • Prevent raw password storage

  • Move toward real authentication

  • Begin persisting users into the database

We are building this correctly — layer by layer.


🗂️ routers/auth.py

from fastapi import APIRouter
from models import Users
from pydantic import BaseModel

router = APIRouter()

class CreateUserRequest(BaseModel):
    username: str
    email: str
    first_name: str
    last_name: str
    password: str
    role: str

@router.post("/auth/")
async def create_user(create_user_request: CreateUserRequest):

    create_user_model = Users(
        email=create_user_request.email,
        username=create_user_request.username,
        first_name=create_user_request.first_name,
        last_name=create_user_request.last_name,
        role=create_user_request.role,
        hashed_password=create_user_request.password,  # temporary
        is_active=True
    )

    return create_user_model

No Changes in other files or folder structure