Streamfinity’s API Makeover — Embracing Python and FastAPI

Explore the journey of a streaming company reinventing its API with FastAPI’s speed and flexibility.

Streamfinity’s API Makeover — Embracing Python and FastAPI
Harper and the FastAPI revolution: Streamfinity’s Journey. Image generated by Midjourney, prompt by author

In the bustling world of digital streaming, a rising star named Streamfinity* aimed to make its mark with an extensive library of movies, exceptional service, and a cutting-edge digital rights management (DRM) offering.

This DRM solution ensured secure content delivery, protecting the intellectual property of filmmakers while providing users with seamless access to their favorite movies.

However, as the company expanded, it became increasingly difficult to maintain its legacy solution based on ASP.NET WebForms. The technological landscape had shifted, and external companies were clamoring for an interface into Streamfinity’s treasure trove of data.

Streamfinity needed a seamless REST API to adapt and flourish, enabling these external partners to effortlessly integrate their movie services into their websites and mobile apps.

Meanwhile, several developers with extensive Python experience have joined the company over the last couple of years, ready to tackle the challenge and help propel Streamfinity to new heights.

That’s when CTO Harper, a visionary leader, stepped in. She boldly decided to seize the opportunity and embrace the Python revolution.

Harper instructed the eager development team to prototype a REST API using Python and FastAPI — a modern, high-performance web framework designed to build APIs quickly.

The development team was overjoyed and excited to embark on a journey to improve Streamfinity’s service and expand its reach and impact across the digital streaming landscape.

Join us in this captivating tale as we explore the transformation of Streamfinity, delving into the world of Python and FastAPI and witnessing the creation of an API that would forever change the company’s destiny under Harper’s guidance.

Feel free to explore the prototype of Streamfinity’s API in this GitHub repository.

*Please note that Streamfinity is a fictional company created for this article to illustrate the practical application of FastAPI in a real-world context.


Table of contents

· Designing the REST API for Streamfinity
Understanding API design principles and best practices
Key API endpoints for streaming and DRM services
· Setting up the development environment
· Creating the database models with SQLModel
Movie and Actor models
Connecting the Movie to the Actor model
· Building the REST API with FastAPI
Connecting to the database
The first route, get an actor by id
Searching for actors
Adding a new actor
Deleting an existing actor
Updating an actor
Implementing the remaining endpoints and routes
· API Documentation
· Securing the API
· Writing test cases for Streamfinity API endpoints
· The Product Demonstration


Designing the REST API for Streamfinity

The development team at Streamfinity gathered for a collaborative session known as a Wolfpack. In this type of meeting, the development team comes together in a single room, working collectively towards a shared objective.

The primary focus of this initial meeting was to explore the design of the core aspects of Streamfinity’s REST API. Harper, the visionary CTO, provided some valuable guidance for the team.

She emphasized the importance of adhering to REST principles, aligning with the needs and expectations of their external partners. Additionally, Harper advised the team to concentrate on the essential functionality rather than attempting to create a comprehensive solution.

Furthermore, Harper emphasized the need to incorporate authentication into the prototype, ensuring that only authorized users and partners could access the API’s critical features.

With these guidelines, the development team eagerly began transforming Streamfinity’s API using Python and FastAPI.

Understanding API design principles and best practices

The Streamfinity team began their journey by identifying design principles for their REST API. Drawing upon the knowledge and experience of team members who had previously worked with REST APIs, they collaboratively formulated the following guiding principles to ensure a robust and user-friendly design.

  • Use standard HTTP methods — Employ standard HTTP methods (GET, POST, PUT, DELETE, etc.) for CRUD (Create, Read, Update, Delete) operations, ensuring consistency and ease of use for API consumers.
  • Resource-based URLs with plurals — Design the API endpoints with clear, resource-based URLs representing specific entities or collections using plurals (e.g., /movies, /actors, /subscriptions).
  • Utilize proper status codes — Return appropriate HTTP status codes to indicate the outcome of an API request (e.g., 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found).
  • Pagination and filtering — Incorporate pagination and filtering capabilities to handle large data sets and allow users to retrieve specific subsets of data efficiently.
  • JSON as the primary data format — Use JSON (JavaScript Object Notation) as the primary data format for API requests and responses, as it is lightweight, easy to read, and widely supported.
  • Clear and comprehensive documentation — Provide thorough and accessible documentation to guide API consumers, including explanations of available endpoints, data formats, and usage examples.

Key API endpoints for streaming and DRM services

With their well-defined API design principles, the Streamfinity team outlined a set of essential REST endpoints. These endpoints would lay the groundwork for Streamfinity’s API, providing a solid and efficient framework to bolster their services.

The team conceived the following core endpoints:

/movies

  • GET: Retrieve a list of movies with optional filtering, sorting, and pagination.
  • POST: Add a new movie to the database.

/movies/{movie_id}

  • GET: Retrieve the details of a specific movie by its ID.
  • PUT: Update the details of a specific movie by its ID.
  • DELETE: Remove a specific movie from the database by its ID.

/actors

  • GET: Retrieve a list of actors with optional filtering, sorting, and pagination.
  • POST: Add a new actor to the database.

/actors/{actor_id}

  • GET: Retrieve the details of a specific actor by their ID.
  • PUT: Update the details of a specific actor by their ID.
  • DELETE: Remove a specific actor from the database by their ID.

/api/movies/{movie_id}/actors/{actor_id}

  • POST: add an actor to a movie
  • DELETE: remove an actor from a movie

/users

  • POST: Register a new user in the system.

/users/{user_id}

  • GET: Retrieve the details of a specific user by their ID.
  • PUT: Update the details of a specific user by their ID.
  • DELETE: Remove a specific user from the database by their ID.

/subscriptions

  • GET: Retrieve a list of available subscription plans.
  • POST: Add a new subscription plan to the database.

/subscriptions/{subscription_id}

  • GET: Retrieve the details of a specific subscription plan by its ID.
  • PUT: Update the details of a specific subscription plan by its ID.
  • DELETE: Remove a subscription from the database by its ID.

/users/{user_id}/subscriptions

  • GET: Retrieve a list of a user's active subscriptions by the user's ID.
  • POST: Add a new subscription for a specific user by their ID.

/users/{user_id}/subscriptions/{subscription_id}

  • PUT: Update the details of a specific user's subscription by the user's ID and subscription ID.
  • DELETE: Cancel a specific user's subscription by their ID and subscription ID.

/token

  • POST: Authenticate a user and generate an access token.

Setting up the development environment

Before the development team could develop the prototype, they had to set up the Python project.

To streamline the process of setting up the development environment for the Streamfinity project, Devon Powers, the team’s DevOps engineer, suggested using Poetry.

Devon was not only a skilled Python developer but also focused on optimizing the build and deployment processes for Streamfinity.

Poetry is a powerful dependency management tool for Python that simplifies package management and virtual environment handling. By leveraging Poetry, the development team could quickly dive into the project without the hassle of manually configuring settings or managing dependencies.

This approach allowed the team to focus on developing features and delivering a high-quality product.

Devon performed the following steps to create the Streamfinity project using Poetry.

  1. Install Poetry: Before the development team could begin using Poetry, they needed to install it. They followed the installation instructions provided in the official Poetry documentation.
  2. Create a new Poetry project: Avery created a new Poetry project for Streamfinity by running the command poetry new streamfinity-fastapi. This command generated a new directory called streamfinity-fastapi with a basic project structure and the essential files, including pyproject.toml, the Poetry configuration file.
  3. Add dependencies: To manage the project’s dependencies, Devon added them to the pyproject.toml file. For example, to add FastAPI and SQLModel, he used the following commands poetry add fastapi and poetry add sqlmodel.
  4. Initialize the virtual environment: To create an isolated virtual environment for the project, the development team ran the following command in the project’s root directory: poetry install. This command installed all the dependencies specified in the pyproject.toml file into a virtual environment.

By setting up the development environment using Poetry, Devon made it easier for the development team to get started on the Streamfinity project. The team could now focus on implementing features and functionality without managing dependencies or manually configuring settings.

New developers joining the team can quickly get up and running by cloning the Streamfinity FastAPI repository and installing the required dependencies using Poetry.

After setting up their local development environment, the new developer can dive into the existing codebase, review the API documentation, and contribute to the project by tackling open issues, implementing new features, or enhancing existing functionality.


Creating the database models with SQLModel

As the Streamfinity team delved into creating database models, Darius Knight, an experienced developer with a background in SQLAlchemy, took the lead in designing the Actor and Movie models.

He was well aware that the many-to-many relationship between these entities was one of the more complex aspects of the core library.

Darius chose to use SQLModel, a powerful tool built upon SQLAlchemy, which seamlessly integrates with FastAPI.

Both frameworks were created by the same developer, Sebastián Ramírez. SQLModel leverages Python-type annotations, Pydantic, and SQLAlchemy to deliver an efficient and expressive experience.

Although Streamfinity’s existing movie and actor models were more comprehensive, the team aimed to create a simplified yet representative version of the Actor and Movie models for their prototype.

To expedite the development process and avoid the need for setting up database servers, the team decided to use SQLite as the database for their prototype.

Movie and Actor models

Darius developed a design approach for the MovieInput and Movie classes, which he presented to the team. He recommended splitting each model class into distinct input and output classes.

The input class, derived from the SQLModel class, would serve as the output class's base, including primary key fields. Darius had seen this method used effectively in a previous project and believed it would simplify input class validation for API endpoints.

After deliberating on Darius’s proposal, the team adopted his design strategy. Darius proceeded to create the MovieInput and Movie classes, as shown below.

It’s important to note that the model employs type hints to facilitate automatic model validation in FastAPI later. However, the current design does not yet include a relationship to an Actor model.

class MovieInput(SQLModel): 
    title: str 
    length: int 
    synopsis: str 
    release_year: int 
    director: str 
    genre: str 
    rating: int | None = None 
 
class Movie(MovieInput, table=True): 
    id: int | None = Field(primary_key=True, default=None)

Additionally, Darius developed the Actor model, which can be seen below.

class ActorInput(SQLModel): 
    first_name: str 
    last_name: str 
    date_of_birth: date 
    nationality: str 
    biography: str | None = None 
    profile_picture_url: str | None = None 
 
class Actor(ActorInput, table=True): 
    id: int | None = Field(primary_key=True, default=None)

Connecting the Movie to the Actor model

It was time to define the relationship between the Movie and Actor models. This involved creating a many-to-many (N:M) relationship, as a movie can feature multiple actors, and an actor can appear in various movies.

Darius knew they needed to introduce a linking table or model that connects the Movie and Actor models to establish a many-to-many relationship. This linking table is also an SQLModel, just like the other models. They decided to name it MovieActorLink, as shown below.

The MovieActorLink model contains only two foreign key fields, which reference the primary keys of the Movie and Actor models.

class MovieActorLink(SQLModel, table=True): 
    movie_id: int = Field(foreign_key="movie.id", 
                          primary_key=True, default=None) 
    actor_id: int = Field(foreign_key="actor.id", 
                          primary_key=True, default=None)

It was time to set up the retrieval of a movie with all its associated actors or all the movies of an actor from the database. To accomplish this, the Movie model would include a list of actors, while the Actor model would have a list of movies.

Both models utilize the Relationship class and the back_populates attribute, referencing the MovieActorLink as shown below. This configuration allows for easy retrieval of related data between the Movie and Actor models.

class Movie(MovieInput, table=True): 
    id: int | None = Field(primary_key=True, default=None) 
    actors: list["Actor"] = Relationship(back_populates="movies", 
                                         link_model=MovieActorLink) 
 
class Actor(ActorInput, table=True): 
    id: int | None = Field(primary_key=True, default=None) 
    movies: list["Movie"] = Relationship(back_populates="actors", 
                                         link_model=MovieActorLink)

The team was thrilled by the simplicity and efficiency of using SQLModel to create and configure relationships between their models. In no time, they had crafted all the necessary model classes (Movie, Actor, User, Subscription).

Check out this GitHub repository to see the full range of model classes that the team developed.


Building the REST API with FastAPI

Avery Maverick, the skilled developer responsible for designing most of the REST API, was eager to take the reins in implementing it using FastAPI. The team meticulously crafted their database models, and Avery was set to showcase her expertise.

Connecting to the database

Her first task was to establish a connection to the SQLite database. To achieve this, Avery created the get_session method, which would be combined with FastAPI's Depends class for dependency injection.

Here's the code snippet for the database connection and the get_session method. Avery employed a generator function with a context manager to ensure that connections to the database were correctly closed after use.

engine = create_engine( 
    "sqlite:///./db/streamfinity.db", 
    echo=True, 
    connect_args={"check_same_thread": False} 
) 
 
def get_session() -> Generator[Session, Any, None]: 
    with Session(engine) as session: 
        yield session

The first route, get an actor by id

With the connection created, Avery focused on structuring the API using FastAPI’s APIRouter class. By separating each API part into different files, she made a clean and organized codebase that would be easy to maintain and extend in the future.

Avery understood the main features of FastAPI, such as automatic validation, documentation, and dependency injection, and knew that utilizing these features would streamline the development process and result in a high-performance, reliable API.

Now, Avery was prepared to bring the API to life. She started with a simple route to retrieve a single Actor by their id. This route involved a few steps.

First, she added the @router.get("/{actor_id}") decorator, which indicated that this function represents a GET method and captures a single path parameter. This allowed the API to handle requests for specific actors based on their IDs.

Then, she injected the SQLModel’s Session using the get_session function. Reading the actor from the database was easily done using the session.get(Actor, actor_id) function.

router = APIRouter(prefix="/api/actors") 
 
@router.get("/{actor_id}") 
def get_actor(actor_id: int, 
              session: Session = Depends(get_session)) -> Actor: 
    actor: Actor | None = session.get(Actor, actor_id) 
    if actor: 
        return actor 
 
    raise HTTPException(status_code=404, detail=f"Actor with id={actor_id} not found")

Searching for actors

Avery’s next task involved creating a route to enable searching for actors based on their attributes. She developed the route below, which utilized optional query parameters to filter actors by features such as name, birthdate, and nationality.

The corresponding filter would be applied to the query using the where method if any of these parameters were provided. The SQLModel's Session was once again injected using the get_session function.

As outlined in the “Key API endpoints for streaming and DRM services” section, the endpoint needed to support sorting and pagination features. Avery added skip and limit query parameters for implementing pagination to accomplish this.

This allowed users to control the number of records returned and the starting point in the dataset. Furthermore, she introduced the order query parameter to facilitate sorting, allowing users to specify the preferred sorting order for the results.

Once the query was constructed based on the provided parameters, it was executed to retrieve the filtered list of actors. The route returned this list, making it easy for users to search for actors based on specific criteria.

@router.get("/") 
def get_actors(name: str | None = Query(None), 
               birthdate: date | None = Query(None), 
               nationality: str | None = Query(None), 
               skip: int = Query(0, description="The number of records to skip"), 
               limit: int = Query(10, description="The maximum nr of records to get"), 
               sort: str = Query("id", description="The field to sort the results by"), 
               order: str = Query("asc", description="The sort order: 'asc' or 'desc'"), 
               session: Session = Depends(get_session)) -> list[Actor]: 
 
    query = select(Actor) 
 
    if name: 
        query = query.where(Actor.last_name == name) 
    if birthdate: 
        query = query.where(Actor.date_of_birth == birthdate) 
    if nationality: 
        query = query.where(Actor.nationality == nationality) 
 
    # Sorting 
    if order.lower() == "desc": 
        query = query.order_by(getattr(Actor, sort).desc()) 
    else: 
        query = query.order_by(getattr(Actor, sort)) 
 
    # Pagination 
    query = query.offset(skip).limit(limit) 
 
    return session.exec(query).all()

Adding a new actor

Avery implemented a route for adding a new actor to the database using the POST method. She crafted the route as can be seen below.

In this route, Avery used the @router.post("/") decorator to indicate that this function represents a POST method for adding a new actor. The response_model parameter was set to the Actor model, ensuring that the response would be automatically validated and serialized based on the model definition.

@router.post("/", response_model=Actor) 
def add_actor(actor_input: ActorInput, 
              session: Session = Depends(get_session)) -> Actor: 
    new_actor: Actor = Actor.from_orm(actor_input) 
    session.add(new_actor) 
    session.commit() 
    session.refresh(new_actor) 
    return new_actor

The add_actor function accepted an actor_input parameter of ActorInput, representing the new actor’s data. The SQLModel’s Session was once again injected using the get_session function.

Avery created a new Actor instance within the function by calling Actor.from_orm(actor_input). This allowed her to convert the input data into an Actor model instance. She then added the new_actor to the session, committed the transaction to persist the data in the database, and refreshed the new_actor instance to retrieve any generated fields, such as primary keys.

Finally, the newly created actor was returned as the response, allowing users to add new actors to the database through the API.

Deleting an existing actor

Avery then implemented a route for deleting an actor from the database. She created a DELETE method to remove a specified actor based on their id to achieve this. She followed these steps:

  1. Added the @router.delete("/{actor_id}", status_code=204) decorator, specifying that this function represents a DELETE method, extracts a single path parameter (the actor_id), and returns a 204 (No Content) status code on successful deletion.
  2. The SQLModel’s Session was once again injected using the get_session function.
  3. She used the session.get method to fetch the Actor based on the provided actor_id.
  4. If the actor was found, the session.delete method was employed to remove the actor from the database and the session.commit method was called to save the changes.
  5. If the actor was not found, an HTTP 404 status code would be sent, indicating that the actor could not be located.
@router.delete("/{actor_id}", status_code=204) 
def delete_actor(actor_id: int, 
                 session: Session = Depends(get_session)) -> None: 
    actor: Actor | None = session.get(Actor, actor_id) 
    if actor: 
        session.delete(actor) 
        session.commit() 
    else: 
        raise HTTPException(status_code=404, detail=f"Actor with id={actor_id} not found")

Updating an actor

Finally, Avery implemented a route for updating an existing actor in the database. She created a PUT method to update the specified actor based on their id and the provided data. To accomplish this, she followed these steps:

  1. Added the @router.put("/{actor_id}", response_model=Actor) decorator, indicating that this function represents a PUT method, extracts a single path parameter (the actor_id), and returns the updated actor data in the response.
  2. She injected the SQLModel’s Session using the get_session function once more.
  3. We utilized the session.get method to fetch the Actor based on the provided actor_id.
  4. If the actor was found, she updated the actor’s attributes with the new data from the provided ActorInput instance and called the session.commit method to save the changes.
  5. If the actor was not found, an HTTP 404 status code would be sent, indicating that the actor could not be located.

Here’s the implemented route:

@router.put("/{actor_id}", response_model=Actor) 
def update_actor(actor_id: int, new_actor: ActorInput, 
                 session: Session = Depends(get_session)) -> Actor: 
    actor: Actor | None = session.get(Actor, actor_id) 
    if actor: 
        for field, value in new_actor.dict().items(): 
            if value is not None: 
                setattr(actor, field, value) 
        session.commit() 
        return actor 
    else: 
        raise HTTPException(status_code=404, detail=f"Actor with id={actor_id} not found")

With this route in place, the team could easily update the details of the actors in the database as needed. The route efficiently handled the update process and returned the updated actor data in the response, ensuring the API remained user-friendly and easy to work with.

Implementing the remaining endpoints and routes

Avery presented the implemented endpoints and routes to the rest of the development team, showcasing how simple the process was using FastAPI. The team was thoroughly impressed with the results and the ease of implementation.

With a clear understanding of the process, the team divided the remaining endpoints among themselves. They implemented all the necessary routes and endpoints in just a few hours to make their application fully functional.

To explore all the other routes, visit the ‘routers’ folder in this GitHub repository. Here, you’ll find all the endpoints of the team's prototype.


API Documentation

As directed by Harper and outlined in the guidelines, Streamfinity’s REST API must provide comprehensive and user-friendly documentation to guide API consumers. This documentation should include explanations of available endpoints, data formats, and usage examples.

Lucas Dubois was assigned the task of exploring the documentation functionality of FastAPI. He conducted a demo to showcase the following aspects:

FastAPI automatically generates documentation for your API endpoints, making it easy for developers to understand the available operations and their usage. By running a FastAPI project and accessing the /docs endpoint, you can view auto-generated documentation that offers an overview of the API endpoints, their supported HTTP methods, request parameters, response status codes, and documentation strings from your code.

A screenshot from a website that shows a list of all the endpoints of Streamfinity’s API.
The documentation FastAPI generated, image by the author

The documentation page lets you interact directly with the API endpoints by clicking “Try it out” and executing requests. This feature eliminates the need for a separate client, simplifying the testing process.

FastAPI generates this documentation based on the OpenAPI Specification, a widely adopted standard for describing RESTful APIs. By visiting the /openapi.json endpoint, you can access the machine-readable OpenAPI Specification JSON document. This document can serve various purposes, such as generating client code or integrating it with other tools.

Additionally, FastAPI provides an alternative documentation format at the /redoc endpoint. This page contains the same information as the /docs endpoint but presents it in a different format and includes a link to the OpenAPI Specification JSON document.

Lucas demonstrated that the openapi.json endpoint link could be easily copied and imported directly into Postman, a tool the development team already knew about.

The team met this feature with great enthusiasm, as it eliminated the need to write additional documentation, a task that only some developers particularly enjoyed.

A screenshot of Postman showing the imported documentation and a list of all available endpoints
Importing the openapi.json from the FastAPI documentation into Postman

Securing the API

As you might remember from the introduction, Harper emphasized the importance of incorporating authentication into the prototype to ensure that only authorized users and partners could access the API’s critical features. Avery chose to leverage FastAPI’s built-in OAuth2 functionality to address this requirement.

Avery utilized the OAuth2PasswordBearer class provided by FastAPI, simplifying setting up the OAuth2 authentication flow by handling access token extraction and format validation. This allowed Avery to focus on implementing the desired authentication mechanism without getting bogged down in the underlying details.

To further streamline the integration of authentication into the API, Avery took advantage of FastAPI’s dependency injection system. By creating a dependency function using OAuth2PasswordBearer, Avery could effortlessly include authentication in any endpoint simply by adding the function as a parameter.

First, she ensured that when adding a new user, the given password would be hashed before storing it in the database.

@router.post("/", response_model=User, status_code=201) 
def add_user(user_input: UserInput, session: Session = Depends(get_session)) -> User: 
    hashed_password = get_password_hash(user_input.password) 
    user_input.password = hashed_password 
 
    new_user: User = User.from_orm(user_input) 
    session.add(new_user) 
    session.commit() 
    session.refresh(new_user) 
    return new_user

The get_password_hash function uses the CryptContext helper from passlib to generate the hash for the password.

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 
 
def get_password_hash(password: str): 
    return pwd_context.hash(password)

Then she added the token endpoint so that users of the API could retrieve a JWT token before accessing a restricted endpoint.

@router.post("/token", response_model=Token) 
def login_for_access_token( 
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()], 
    session: Session = Depends(get_session) 
): 
    query = select(User).where(User.email == form_data.username) 
    user: User | None = session.exec(query).first() 
 
    if user is None: 
        raise_401_exception() 
 
    assert user is not None 
 
    if not verify_password(form_data.password, user.password): 
        raise_401_exception() 
 
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 
    access_token = create_access_token(data={"sub": user.email}, 
                                       expires_delta=access_token_expires) 
 
    return {"access_token": access_token, "token_type": "bearer"} 
 
def raise_401_exception(): 
    raise HTTPException( 
        status_code=status.HTTP_401_UNAUTHORIZED, 
        detail="Incorrect username or password", 
        headers={"WWW-Authenticate": "Bearer"}, 
    )

Finally, Avery integrated the authentication requirement into the necessary endpoints, ensuring only authorized users with valid tokens could access restricted resources. For instance, she added authentication to the AddActor endpoint.

This was achieved by implementing a new get_current_user function and injecting it into the add_actor function using FastAPI's dependency injection system. This ensures that the endpoint is accessible only to authenticated users with a valid token, thus enhancing the security of the API.

@router.post("/", response_model=Actor, status_code=201) 
def add_actor(actor_input: ActorInput, 
              current_user: User = Depends(get_current_user), 
              session: Session = Depends(get_session)) -> Actor: 
    new_actor: Actor = Actor.from_orm(actor_input) 
    session.add(new_actor) 
    session.commit() 
    session.refresh(new_actor) 
    return new_actor

The get_current_user function takes the access token from the request header, decodes and verifies it using the jwt.decode function from the jose library.

If the token is valid, it extracts the user's identifier (e.g., username or email) from the token's payload and retrieves the user's information from the database or another data source. If the token is invalid or the user is not found, it raises an HTTP 401 Unauthorized exception.

def get_current_user(token: str = Depends(oauth2_scheme), 
                     session: Session = Depends(get_session)) -> User: 
    try: 
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 
        username: str | None = payload.get("sub") 
        if username is None: 
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, 
                                detail="Could not validate credentials") 
    except JWTError: 
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, 
                            detail="Could not validate credentials") 
 
    query = select(User).where(User.email == username) 
    user: User | None = session.exec(query).first() 
    if user is None: 
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, 
                            detail="User not found") 
 
    return user

Avery completed her presentation to the development team, showcasing the implemented authentication functionality and explaining how she integrated it into the application. She also noted that while she liked the overall integration, she wasn’t entirely satisfied with encoding and decoding JWT tokens manually.

During her research, Avery discovered several external libraries and plugins, such as fastapi-jwt-auth, that could streamline the JWT token management process. These libraries offer additional tools and decorators to simplify JWT handling, making the process more convenient and less error-prone.

However, due to time constraints, Avery didn’t have the opportunity to test and integrate these external plugins into the current implementation. She suggested that the development team could explore these options to optimize the authentication process further and make it more efficient.


Writing test cases for Streamfinity API endpoints

As the development of the Streamfinity API progressed, the whole team knew that ensuring proper functionality and reliability was essential. To achieve this, they decided to work together to write test cases for all the API endpoints.

The team used FastAPI’s built-in TestClient to create their test cases. This powerful tool enabled them to write test functions for each endpoint, covering various scenarios. They thoroughly tested actions such as creating, retrieving, updating, and deleting resources and checking authentication and authorization.

Here’s an example of a test case for the add_actor endpoint:

from fastapi.testclient import TestClient 
 
client = TestClient(app) 
 
def test_add_actor(): 
    actor_data = test_actor.dict() 
    actor_data["date_of_birth"] = test_actor.date_of_birth.isoformat() 
    response = client.post("/api/actors/", json=actor_data, 
                           headers={"Authorization": f"Bearer {access_token}"}) 
    assert response.status_code == status.HTTP_201_CREATED 
    assert response.json()["first_name"] == test_actor.first_name 
    assert response.json()["last_name"] == test_actor.last_name

Leveraging the TestClient enabled the team to work together efficiently and thoroughly test every aspect of the API. This collaborative approach facilitated the identification of potential issues and allowed for their resolution before deployment, leading to a more robust and dependable API.

All the tests created by the team can be found in the Streamfinity prototype’s GitHub repository.


The Product Demonstration

After an intense week of rapid development, the team was ready to unveil the prototype REST API of Streamfinity to Harper, the company’s Chief Technology Officer. The demonstration marked a significant milestone and culminated the team’s efforts.

The development team adeptly used Postman, a popular API client, to showcase the various endpoints of the API. The presentation was not only about the features but also demonstrated the ease of use and the versatility of the API. Harper was shown how different functionalities could be triggered, and data could be manipulated through these endpoints.

In addition to the live demonstration, the team presented the comprehensive documentation FastAPI had generated. This documentation detailed the different aspects of the API, elaborating on its functionalities, usage, and other relevant details.

One of the highlights was how the documentation could be seamlessly imported into Postman. This feature would provide a valuable resource for potential customers, offering detailed guidance and practical API examples.

A palpable enthusiasm among the development team marked the demonstration. They were particularly excited about the capabilities of FastAPI, the modern, fast (high-performance) web framework they used to build the API. The consensus was unanimous — this prototype deserved to be taken forward, and a first beta version of a part of the API should be developed.

However, the team also acknowledged that areas still required further exploration. One key aspect was the plugin for authentication — an essential feature for ensuring the security and integrity of the API. The team also acknowledged the need to investigate how to deploy and run a FastAPI application in a production environment.

These areas were still relatively uncharted territory, and further research and development were necessary.

Stepping up to the challenge, Devon Powers, the team’s DevOps engineer, volunteered to delve deeper into these aspects. Armed with a keen interest and the necessary technical skills, Devon was ready to tackle the complexities of deployment and investigate how to deploy seamlessly. His research and subsequent findings would be instrumental in taking the project to the next level.

Devon’s journey into these technical depths will not go unnoticed. His explorations, challenges, and triumphs will be the focus of an upcoming article. This detailed narrative will illuminate the intricate process of deploying a FastAPI application into a production environment.

So, stay tuned for an insightful deep dive into the world of DevOps as we follow Devon’s journey in our next feature.

In conclusion, the demonstration was a success, marking the first significant step toward realizing Streamfinity’s REST API. The team, buoyed by the successful demonstration, was eager to continue their work, solve the pending challenges, and bring the API closer to its final form.

Feel free to explore the prototype of Streamfinity’s API in this GitHub repository.

Happy coding!