Setting Up User Profiles in FastAPI

undraw svg category

Welcome to Part 9 of Up and Running with FastAPI. If you missed part 8, you can find it here .

This series is focused on building a full-stack application with the FastAPI framework. The app allows users to post requests to have their residence cleaned, and other users can select a cleaning project for a given hourly rate.

Up And Running With FastAPI

In the previous post we implemented a proper login flow using FastAPI’s built in OAuth2 system. On top of that, we built out dependencies that ensure users can access protected routes using JSON Web Tokens. With authentication out of the way, we can focus on user presence and how users interact with our application. We’ll be building out user profiles and ownership, which is a non-trivial task. This post will be particularly long and most solutions will simply require us to bust out some SQL, so get ready.

Let’s begin by giving users the ability to customize their profiles.

Creating User Profiles

We’ll need to update our database to support profiles, as we’ll want to give our users the ability to customize their online presence and interact with other user’s profiles. Every user’s id serves as a foreign key for items they own, and profiles are no different. So we’ll need to account for that as well.

As with any modification to the database, we start with the migrations file.

Migrations

Before we make any adjustments, let’s roll back our migrations.

Now go ahead and open up the file that looks like: db/migrations/versions/12345678654_create_main_tables.py.

A couple things to note here. We’re adding a profiles table to our database that stores supplementary information about a user. We’re using the SQLAlchemy’s sa.ForeignKey table constraint to specify that each record in the profiles table belongs to a record in the users table.

While it’s often convenient to keep all user-related information in a single table, we aren’t taking that approach here. Instead, we’ll keep authentication information in the users table and personal information in the profiles table. When we want to get both, we’ll simply join the tables in our SQL query.

In our case, the flexibility of this approach outweighs the cost of joining the tables whenever a user is queried. We’re also able to add an arbitrary number of columns to the profile model and attach an unlimited number of profile types to a single user.

At the end of the file, we’ve adjusted the upgrade and downgrade functions for our migrations runner.

With that out the way, it’s time to migrate the database by entering the container like before (unless we’re already there) and running the alembic upgrade head command.

Profile Models

Let’s go ahead and create models for our user profiles.

Create a new file in the models directory called profile.py.

And inside that file, add the following:

models/profile.py

Nothing crazy going on here.

The profile is standard, so most of the work is done by inheriting from our base model and mixins. Though our profiles table doesn’t have a username field or an email field, we still add them to the ProfileInDB model. The ProfilePublic model inherits them as well. Depending on the situation, this may be useful for displaying user profiles in our UI. We’ve also specified that the image must by an http url - validated for us by pydantic.

Testing User Profiles

Let’s make some tests. We want tests that ensure a profile is created for a user when they register, that users can see other users’ profiles when they’re authenticated, and that users can update their own profile. In a future post, we’ll add a social component to our application and test that as well.

We’ll do this in pieces, taking it one step at a time.

Start, by creating a new file called test_profiles.py.

And add the following to it.

tests/test_profiles.py

Baby steps here, as we’re only checking to see if 2 routes exists: one to fetch a profile by a user’s username and one to update a user’s own profile. We’ve also started using from fastapi import status for our status codes. No specific reason for doing it this way. We simply take this approach because it requires fewer lines than importing status codes from starlette directly.

Run the tests and watch them fail.

Making these two tests pass is easy enough. Let’s create a new route file in the api/routes/ directory.

And we can start adding to it like so:

api/routes/profiles.py

We’ve defined a GET route and a PUT route for fetching and updating profiles, respectively. No logic here yet, as they both simply return None. The only thing to pay attention to is that we’re validating the username in the same way as in our UserCreate and UserUpdate models. It must be at least 3 characters long and consist of only letters, numbers, underscores and dashes.

We’ll need to register this new router with our api router so open up the api/routes/__init__.py file.

api/routes/__init__.py

Same as before, we attach our profiles router to the api router under the /profiles namespace.

Now when we run our tests again they should pass.

Let’s move on and add the next test class to our test_profiles.py file.

tests/test_profiles.py

As soon as we try to run our tests, we’ll get an import error. We don’t have a ProfilesRepository yet, so let’s make one.

And let’s build it out with two new fancy methods.

db/repositories/profiles.py

Our new ProfilesRepository is now ready for use. It can create profiles for new users and fetch a profile when provided the user_id. If we run our tests now, we should see that our latest test is failing. When we attempt to fetch the newly created user’s profile, we get None. Let’s make sure that when a new user is created, our UsersRepository also creates a profile for that user.

Open up the db/repositories/users.py file and update it like so:

db/repositories/users.py

This is a useful pattern that we’ll take advantage of regularly. By adding the ProfilesRepository as a sub-repo of the UsersRepository, we can insert any profile-related logic directly into our user-related logic. And we do just that here. Once a user registers with our application, we take the newly created user’s id and use it to add an empty profile to our database. If we want to allow users to sign up with additional information, we can pass that along here as well.

Run the tests again and they should all pass.

Fetching and Updating Profiles

Let’s flesh out those two empty routes we created earlier, starting with some tests.

In the conftest.py file, add a new fixture.

tests/conftest.py

Now, let’s use it in our test_profiles.py file.

tests/test_profiles.py

In the first test, we check to see if test_user can access the profile of test_user2. Since our authorized_client uses the JWT token for test_user, this is relatively straightforward to implement. In the second test, we attempt to do the same thing, except with an unauthorized client. We expect to see it fail. Our third test simply ensures that a 404 is returned when the username has no corresponding profile.

Run the tests and watch them fail.

Let’s start in the api/routes/profiles.py file. Here’s the updated file.

api/routes/profiles.py

Run the tests again and the last one should pass. Simply by including the get_current_active_user dependency, we protect this route from unauthenticated requests. The other two tests are failing because we haven’t implemented the get_profile_by_username method on our ProfilesRepository.

This will be mostly an exercise in SQL, so let’s get to it.

db/repositories/profiles.py

Before we get to the SQL, let’s talk about what we’re trying to accomplish. We want to take in a username and check in our database for any user with that username. If we find that username, we want to grab their email and username. Then we want to attach it to the profile associated with that user and return the ProfileInDB model with all of the attributes.

To make that all happen, we join the profiles and users table together for a user that matches the sub-query:

We then only select the username and email from the users table, while selecting all fields from the profiles table.

Run the tests again and they should all pass.

Attaching Profiles to UserPublic Models

Let’s refactor our application models a bit. We’re going to attach public user profiles to user models that are returned by our user routes.

Start with the models/user.py file and update it like so:

models/user.py

Now we have the ability to attach a user profile to our UserPublic models. Seems simple enough, right? Though it’s a small change, implementing this update will require modifications in a few places across our codebase. Check it out.

In the UsersRepository add a new method that makes it easy to populate the user with their profile.

Here’s the whole file in its entirety.

db/repositories/users.py

Wow, ok. A lot of changes here.

The key piece is our new populate_user method which takes advantage of the profiles_repo to attach a user’s profile onto the UserPublic model. In both our get_user_by_email and get_user_by_username methods we’ve added a new populate parameter that determines whether or not we should simply return the UserInDB model or return the result of calling our populate_user method. Adding this parameter is nice, because it means that when we don’t need the user’s profile or actually want to access the user’s password and salt - like in our authenticate_user method - we can set populate=False and only get the UserInDB model back.

There’s a problem though. This refactor works, which we’ll see in a minute. But if we run our tests, many of them are failing.

The main problem is with our authentication service. Open up services/authentication.py and update it like so:

services/authentication.py

When creating access tokens, we were previously checking that the user we passed was an instance of UserInDB. Since that might not always be the case, we’re switching to the parent class that both UserInDB and UserPublic inherit from - UserBase. This ensures that our access token is created for instances of both models without failure.

We’re going to need to update our tests to reflect the new changes as well, so open up the tests/test_users.py file and modify it like so:

tests/test_users.py

Alright, nothing too serious here. Mostly updating the get_user_by_email methods to use the new populate parameter and set it to False in both tests to keep the same behavior we were expecting before. We also exclude the new profile attribute on our UserPublic model that reflects the changes we’ve made up to this point.

The ability to easily manipulate pydantic models will prove useful time and time again. In fact, let’s do it one more time and fix the last issue we’ll get when returning newly registered users.

Open up the api/routes/users.py file and make one minor change.

api/routes/users.py

Since we’re now returning a UserPublic model upon registration, we can simply update the access_token attribute with our new token and return that user. Simple enough.

Run the tests again and they should all be passing.

And voila! Users should now have their profiles attached when they are returned from our API. Try it out in the interactive docs at localhost:8000/docs.

Updating Profiles

Now let’s make sure users can update their own profiles.

Create a new test class in test_profiles.py.

tests/test_profiles.py

Run the tests and watch them fail.

Open up the api/routes/profiles.py file and update the PUT route like so:

api/routes/profiles.py

We’re simply calling the currently non-existent update_profile method and passing along whatever updates are needed along with the user that is being updated.

Now on to the repo.

db/repositories/profiles.py

Run the tests again and this time they should pass.

Wrapping Up and Resources

This was the longest post so far, so readers who feel exhausted by this point should not feel bad. Our API is really taking shape, and we’re ready to put the finishing touches on an MVP. In the next post, we’ll ensure users own cleanings they create and refactor all of the associated routes and repository methods.

Github Repo

All code up to this point can be found here:

Special thanks to James Cape and Erick Cavalie for correcting errors in the original code.

Tags:

Previous Post undraw svg category

Auth Dependencies in FastAPI

Next Post undraw svg category

User Owned Resources in FastAPI