DEV Community

lou
lou

Posted on • Edited on

A Step by Step Guide to Building Lightning Fast APIs

FastAPI is a Python framework that lets you build APIs with speed and simplicity. It supports async programming and that's what makes it fast.
In this guide, I’ll walk you through creating your first FastAPI project, enhancing it with data models, integrating a database, and even securing your endpoints with JWT authentication.

A Dummy FastAPI Project

Let’s kick things off with a basic API:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}


@app.get("/hello/{name}")
async def say_hello(name: str):
    return {"message": f"Hello {name}"}

Enter fullscreen mode Exit fullscreen mode

You might be wondering, what’s happening here?
We Created:

  1. An instance of the class FastApi from the module fastapi.
  2. A variable, app, which will serve as the point of interaction to create the api.
  3. Used decorator, @app followed by the HTTP method, get, put, post, delete
  4. Passed the endpoint to the decorator and defined the operation function

Try it out by running your server and visiting

http://127.0.0.1:8000/docs

To explore the interactive documentation

Your First FastAPI Project

Now let's create a model to hold the schema of our data type. There's one problem with using plain Python classes for this: FastAPI won't know how to handle these models automatically, meaning you'll have to manually parse and validate the request body. To avoid this, use Pydantic's BaseModel as recommended in their documentation.

Let's explore how to add a Starship to our API by defining its data type using a Pydantic model:

First, we import BaseModel from Pydantic.
Then, we define the Starship class by inheriting from BaseModel and listing out the attributes.
This method automates data validation, making the API easy to maintain.

We’ll dive into the endpoint creation details as we move along. For now launch the following URL to test things out:

http://127.0.0.1:8000/docs#

This interactive docs page lets you see your endpoints in action. All you have to do is simply add the required details and watch the results:

Image description

Now, let’s spice things up by adding a Jedi to link each Starship to its purchaser. First, we need to define the fields. Pydantic’s Field is a lifesaver here, it not only lets you add extra information to a field but can also use a default_factory to auto-generate values. This is perfect for automatically creating an ID whenever a new Starship or Jedi is instantiated.

Additionally, you can use Field to make your API documentation richer by adding descriptions. For more details on the available parameters, check out the Pydantic documentation

Note the use of ellipsis within Field. It emphasize on the fact that a value must be provided.

Schema Definition

Since the ID is auto generated, having a well defined schema helps prevent unnecessary inputs from the client. In Pydantic v2, you should now define the JSON schema using model_config = ConfigDict() instead of the deprecated json_schema_extra.

For fields that are computed from other fields, simply add the @computed_field decorator to the property function. It’s recommended to explicitly use the @property decorator, but it isn’t required.

Now let's explore some more Pydantic types, and add them to our code.

HttpUrl is a type that accepts http or https URLs.

We will be using it to provide a list of Photos to show all of the angles of the Starship for a 360 view.

We will be using Form for sign in purposes


Let's try it out

Image description
Awesome!

Integrating a Database with SQLAlchemy and SQLite

Now that we want to save the data sent by the client, it's time to integrate a database into our app. We'll walk through the steps of using SQLAlchemy—a popular ORM for Python—together with SQLite. We’re keeping it simple: create a new directory (let’s call it starship_db), set up a virtual environment if you haven’t already, activate it, and run the following command to get started.

pip3 install sqlalchemy fastapi pydantic

Let's set up your project files with a few quick terminal commands. This step creates your SQLite database file and organizes your project structure:

touch starship.db
mkdir starship
cd starship
touch main.py database.py models.py schema.py

The models.py will hold your models, each model is a blueprint for a database table.

Here the Starship class serves as a blueprint for a database table. By setting tablename = 'starship' you're instructing SQLAlchemy to create a table in your database named "starship"

Next take your existing Starship model and add it into your schema.py file to define a response model called StarshipResponse. In this model, make sure to include orm_mode=True this tells FastAPI to serialize your SQLAlchemy objects to JSON, ensuring smooth and accurate API responses.

Managing Database Migrations with Alembic

We’ll use Alembic as our go to migration tool to manage changes to our database schema and keep everything running smoothly as our code grows. Alembic makes it much easier to handle schema updates without the headache of manual migrations.

You need to install Alembic and initialize it

pip install alembic
alembic init alembic

This command will create a new directory named alembic in your project and an alembic.ini file. Inside the alembic directory, there will be env.py file and a versions directory for your migration scripts.

Inside of your alembic.ini file, update sqlalchemy.url
then go to alembic/env.py and change this line

target_metadata = None

to your model's metadata, something like this:

from starship.database import Base
target_metadata = Base.metadata
Enter fullscreen mode Exit fullscreen mode

This will link Alembic with your models and allows it to detect any changes in the models when generating migration scripts.

Initialize the migration with:

alembic revision --autogenerate -m "Init"

And apply it with:

alembic upgrade head

Now let's set up our database connection and session, and define a helper function to yield a session instance. This ensures that your database connections are managed properly, cleaning up after each request:

Defining CRUD Operations for Starships

In your main.py, you'll use the get_db() method to obtain a database session, and then define a POST endpoint to create a new Starship. This endpoint takes the input object, instantiates the model, adds the new entry to the database, commits the transaction, and finally returns the created record. This approach is fundamental for populating your database via API requests.

db.add(starship)
db.commit()
db.refresh(starship) 
Enter fullscreen mode Exit fullscreen mode

And just as we define a POST method to add data. We need to define a PUT, GET and DELETE methods for the rest of the operations. After all, managing your data means you'll also need to update, retrieve, and remove entries as needed.
For these operations we will use filter() to search for a specific starship in the database.

Now if we try it out:

Image description

Defining Relationships: Connecting Starships to Their Pilots

Every starship needs a pilot. Let's create a User model to represent the pilots who navigate these ships and implement the same CRUD operations as we did for Starships.



Now, let's connect the User with their Starship. A pilot can fly more than one starship at different times. They can fly the Falcon one day and the X-Wing the next. This illustrates a one to many relationship between the User and their Starships.

Image description

In our code this translates to adding the ForeignKey for User into our Starship and define a relationship to User. In User we will define a relationship with Starship, here back_populates keyword tells SQLAlchemy that there is a relationship on the User side.

Routes with APIRouter

Previously, we used app to define our routes directly. This can become cumbersome as we write more code. Instead, we can group related routes together using APIRouter with tags. In addition to that, with APIRouter, we can avoid repeating the base URL for each route, by defining a prefix.
It's as simple as importing APIRouter and defining tags and prefix:

from fastapi import APIRouter

router = APIRouter(tags=["starships"], prefix='/starship')

Enter fullscreen mode Exit fullscreen mode

After you're done, let's do some clean up of our previous code, we will replace the @app decorators with @router. Since you've defined a prefix (/starship) in our router, a route defined as:

@router.post('/')

Enter fullscreen mode Exit fullscreen mode

will be accessible at '/starship' rather than '/'

Authenticating our pilots

Let's create our Sign In endpoint.

First define the new models:

We now need to define two essential functions for our authentication system:

generate_token: This function copies the provided data, adds an expiration timestamp, and encodes everything into a JWT using our secret key and algorithm.

get_auth_user: This function uses oauth2_scheme to extract the token from the request header, decodes it, and verifies the user's email. It can be used as a dependency to protect other routes.

But before we can do any of this, we need a secret key. To generate a secure random hexadecimal string, run:

openssl rand -hex 32
Enter fullscreen mode Exit fullscreen mode

We'll use this generated string as our SECRET_KEY and HS256 as our algorithm.

Below is the complete code for these functions:

Now onto our Sign In endpoint. What we need to handle is when a user submits their credentials (email and password), the endpoint should be able to:

Query the database to retrieve the user.
Verify the password.

pwd_context.verify()
Enter fullscreen mode Exit fullscreen mode

If both the email and password are correct, generate a JWT token using our generate_token function.
Return the JWT token as an access token along with the token type 'bearer'.

Let's try it out:

Image description

Awesome! Now that we are able to generate our JWT tokens, we should be able to use it to secure our endpoints. Let's tackle our previous starship endpoint.
Take this operation for instance:

@router.get('/', response_model=List[StarshipResponse])
def get_starships(db: Session = Depends(get_db), auth_user: User = Depends(get_auth_user)):
    return db.query(models.Starship).all()

Enter fullscreen mode Exit fullscreen mode

The get_starships function includes this line now

 auth_user: User = Depends(get_auth_user) parameter.
Enter fullscreen mode Exit fullscreen mode

This dependency ensures that only authenticated users can access the endpoint. If a request does not have valid authentication credentials, FastAPI automatically returns a 401 Unauthorized response.

If you go to your docs you will notice a lock

Image description

And If you try it out and hit execute it will return unauthorized error

{
"detail": "Not authenticated"
}

Great, that means that your endpoint is now secured. To successfully access this secured endpoint, you need to provide a valid JWT token in the Authorization header of your request. The header should be formatted as follows:

Authorization: Bearer your_jwt_token

Enter fullscreen mode Exit fullscreen mode

I hope this guide has been comprehensive. If you have any questions or need further clarification, please leave a comment below.

Top comments (0)