Python API Development with FastAPI

Advanced FastAPI

User Registration Path Operation

In this article, we extend our previous implementation for posts by adding a new path operation to create a user. While the process is similar to creating a post, there are key modifications needed to handle user-specific details like email and password.

Below is the original posts creation endpoint for reference:

@api.post("/posts", status_code=status.HTTP_201_CREATED, response_model=schemas.Post)
def create_posts(post: schemas.PostCreate, db: Session = Depends(get_db)):
    # cursor.execute("""INSERT INTO posts (title, content, published) VALUES (%s, %s, %s)
    #                   RETURNING * """,
    #                   (post.title, post.content, post.published))
    # new_post = cursor.fetchone()
    # conn.commit()

    new_post = models.Post(**post.dict())
    db.add(new_post)
    db.commit()
    db.refresh(new_post)

    return new_post
app/models.py", "C:\Users\sanje\Documents\Courses\fastapi\app\models.py.915f4585d116cbddab21f73e5527481.tmp": Reloading...
Database connection was successfull
INFO:     Started server process [27704]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Updating Data with SQLAlchemy

Later in the article, we update an existing post. The following snippet retrieves a post by its ID, raises an exception if it's not found, applies updates using new data, commits the changes, and returns the updated post:

post_query = db.query(models.Post).filter(models.Post.id == id)
post = post_query.first()

if post is None:
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
                        detail=f"post with id: {id} does not exist")

post_query.update(updated_post.dict(), synchronize_session=False)

db.commit()

return post_query.first()
INFO:     Started server process [27704]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Refactoring the Endpoint for User Creation

To create a new user, modify the decorator to target the /users URL. In this refactoring, we switch from handling posts to managing user registration. For simplicity, we temporarily remove the response model.

Below is the refactored endpoint for user creation:

@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    new_user = models.User(**user.dict())
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    return new_user
WARNING:  WatchGodReload detected file change in '['C:\\Users\\sanje\\Documents\\Courses\\fastapi\\app\\main.py.dbfc928ea7d1f64e03ca5f2a29d1b.tmp']: Reloading...
INFO:     Database connection was successful!
INFO:     Started server process [2520]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

The function create_user clearly aligns with the endpoint's purpose. Remember that when creating any resource, the default status code should be 201.

Defining the User Schema

When receiving registration data, the incoming JSON must include both an email and a password. We create a dedicated Pydantic schema to enforce required fields. Below are the model definitions for posts and the initial user creation schema:

class PostCreate(PostBase):
    pass

class Post(PostBase):
    id: int
    created_at: str

    class Config:
        orm_mode = True

class UserCreate:
    email: str
    password: str

For additional validation of email addresses, we use Pydantic’s EmailStr. This ensures that the provided email follows a valid format. Make sure the email-validator library is installed (it comes automatically when installing FastAPI with the all flag, or you can install it via pip install email-validator).

Below is an updated version that uses EmailStr and improves date handling for posts:

from pydantic import BaseModel, EmailStr
from datetime import datetime

class PostBase(BaseModel):
    title: str
    content: str
    published: bool = True

class PostCreate(PostBase):
    pass

class Post(PostBase):
    id: int
    created_at: datetime

    class Config:
        orm_mode = True

class UserCreate(BaseModel):
    email: EmailStr
    password: str
ani58061=7.0.0
async-exit-stack=1.0.1
async-generator=1.10
autotext=1.5.7
certifi=2021.5.30
charset-normalizer=2.0.4
click=7.1.2
colorama=0.4.4
dnspython=2.1.0
email-validator=1.1.3
fastapi=0.68.0
graphene=2.1.8
graphql-core=2.3.2

The updated schema validates the email field properly. You should see email-validator when running pip freeze.

Note

Returning a password in the response is not secure. Always ensure that sensitive information is excluded from API responses.

Implementing the Create User Endpoint

Back in the main application file, define the endpoint to create a new user by adapting the logic from the post creation operation. Notice how we convert the incoming Pydantic object to a dictionary and unpack it when instantiating the SQLAlchemy model, which facilitates proper insertion.

The following snippet demonstrates the user creation endpoint:

@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    new_user = models.User(**user.dict())
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    return new_user

Again, observe the console messages as the server reloads and confirms the database connection:

WARNING:  WatchGodReload detected file change in '['C:\\Users\\sanje\\Documents\\Courses\\fastapi\\app\\main.py.dfbc928ea7d1f64e03ca5f2a29d1b.tmp']: Reloading...
INFO:     Database connection was successful!
INFO:     Started server process [2520]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Testing the Endpoint via Postman

Use an API client like Postman to test the endpoint. Create a new POST request named "create user" and set the request body to raw JSON. For example, use the following payload:

{
    "email": "[email protected]",
    "password": "password123"
}

Ensure that the URL points to /users instead of /posts. When the request is sent, you should receive a response containing the user's email, creation timestamp, and ID. For instance:

{
    "email": "[email protected]",
    "password": "password123",
    "created_at": "2021-08-22T21:50:19.255781-04:00",
    "id": 4
}

To verify that the user was created, run an SQL query against the users table:

SELECT * FROM public.users
ORDER BY "id" ASC;

Alternatively, simply:

select * from users;

Validating Email Input

To ensure the email validator is working, try submitting a request with an invalid email format. In this case, the schema validator returns an error message similar to:

{
    "detail": [
        {
            "loc": [
                "body",
                "email"
            ],
            "msg": "value is not a valid email address",
            "type": "value_error.email"
        }
    ]
}

After correcting the email—for example, using "[email protected]"—the endpoint successfully creates and returns the user.

Creating a Response Model for the User

To prevent sensitive information (like the password) from being returned in the API response, define a new Pydantic model called UserOut. This model includes the user ID, email, and optionally the creation timestamp.

class UserOut(BaseModel):
    id: int
    email: EmailStr

    class Config:
        orm_mode = True

Now, update your user creation endpoint to use the UserOut response model:

@app.post("/users", status_code=status.HTTP_201_CREATED, response_model=schemas.UserOut)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    new_user = models.User(**user.dict())
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    return new_user

When a new user is created, the response will include only the fields defined in UserOut. For example:

{
    "id": 6,
    "email": "[email protected]"
}

To include all desired fields (such as created_at), ensure that your models are updated accordingly:

class PostCreate(PostBase):
    pass

class Post(PostBase):
    id: int
    created_at: datetime

    class Config:
        orm_mode = True

class UserCreate(BaseModel):
    email: EmailStr
    password: str

class UserOut(BaseModel):
    id: int
    email: EmailStr

    class Config:
        orm_mode = True

This configuration guarantees that the response model accurately represents the stored data without exposing sensitive details like the password. With these improvements, your API now securely processes user registration by enforcing email validation, handling user data safely, and returning only the necessary information to the client.

Watch Video

Watch video content

Previous
Creating User Table