Introduction
Welcome to this beginner-friendly guide on embarking upon the realms of web development by building a simple yet robust API with Python and FastAPI. In this tutorial, we will delve into the process of setting up a FastAPI project, creating all the necessary endpoints for a TODO application, including GET, POST, PUT, and DELETE requests.
We’ve chosen SQLite as our database to simplify the persistence layer, allowing you to focus more on understanding the FastAPI framework and less on database configurations. SQLite is a self-contained, serverless, and zero-configuration database engine, which makes it a great choice for beginners and for the purpose of this tutorial.
…
By the end of this tutorial, you’ll have a working web API deployed locally on your machine. The complete code for this tutorial is available on GitHub.
Why FastAPI?
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. It is designed to be easy to use, efficient, and scalable. FastAPI comes with built-in support for documentation generation and automated testing, making it an excellent choice for building robust web applications.
Setting Up Your Environment
Before we dive into building our TODO application, let’s make sure we have all the necessary tools and libraries installed. First, make sure you have Python 3.7 or higher installed on your machine. You can check your Python version by running the following command in your terminal:
python --version
If you don’t have Python installed, you can download and install it from the official Python website.
Once you have Python installed, you can create a virtual environment for your project. Open your terminal and navigate to the directory where you want to create your project. Then run the following commands:
python -m venv myenv source myenv/bin/activate
This will create a new virtual environment and activate it. Now, you can install FastAPI, Uvicorn, SQLAlchemy, and other required libraries by running the following command:
pip install fastapi uvicorn sqlalchemy
Configuring the Database
We will use SQLite for this tutorial. Let’s start by setting up our database and ORM. Create a new file database.py
and add the following code:
# database.py from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker DATABASE_URL = "sqlite:///./test.db" Base = declarative_base() engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def init_db(): Base.metadata.create_all(bind=engine)
DATABASE_URL = "sqlite:///./test.db"
- this line sets up a constant named
DATABASE_URL
that holds the connection string to the database. In this case, it’s connecting to a SQLite database namedtest.db
located in the current directory.
Base = declarative_base()
:
- This line creates a base class for declarative class definitions. The
declarative_base()
function returns a new base class from which all mapped classes should inherit. By using thedeclarative
system, you can create classes that include directives to describe the actual database table they will be mapped to.
engine = create_engine(DATABASE_URL)
:
- This line creates a new instance of an
engine
, which is the source of connectivity to the database. Thecreate_engine()
function takes the database URL, in this caseDATABASE_URL
, and returns an instance ofEngine
. Thisengine
will be used to interact with the database.
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
:
- This line creates a custom
sessionmaker
, which is a factory for producing session objects, which are the SQLAlchemy equivalent of a “workspace” for our operations. - The
autocommit
argument is set toFalse
to ensure that the session does not commit automatically, allowing you to have better control over when transactions are committed to the database. - The
autoflush
argument is set toFalse
to ensure the session does not flush automatically, which means that it will not synchronize the session’s state with the database unless explicitly told to do so. - The
bind
argument binds thesessionmaker
to ourengine
, so that the sessions it creates are connected to our database.
def init_db():
:
- This line defines a function named
init_db
which will be used to initialize the database.
Base.metadata.create_all(bind=engine)
:
- Inside the
init_db
function, this line creates all of the tables defined on theBase
class and its subclasses, which includes anyTable
objects that might be associated with theBase
and its subclasses. - The
bind
argument specifies theengine
that will be used to talk to the database, so this line is essentially telling SQLAlchemy to create all of the necessary database tables using the specifiedengine
.
This setup is a simplified way of establishing a connection to a database, defining a way to create new sessions (which represent transactions), and initializing the database with the required tables.
Defining Database Models
In order to interact with the database, we need to define our data models. We’ll create a new file named models.py
where we’ll define a SQLAlchemy model for our TODO items. This way, we keep our project organized and separate the database schema definition from our main application logic.
Create a new file models.py
and add the following code:
# models.py from sqlalchemy import Column, Integer, String, Boolean from database import Base class TodoInDB(Base): __tablename__ = "todos" id = Column(Integer, primary_key=True, index=True) task = Column(String, index=True) completed = Column(Boolean, default=False)
Now, TodoInDB
is our database model representing a TODO item with three fields: id
, task
, and completed
. The id
field is our primary key, task
is a string representing the task description, and completed
is a boolean indicating whether the task has been completed or not.
Creating the TODO Application
Now that we have our environment and database set up, let’s start building our TODO application. We will create a new Python file called main.py
and import the necessary modules:
# main.py from fastapi import FastAPI, HTTPException, status, Depends from pydantic import BaseModel from sqlalchemy.orm import Session from models import TodoInDB from database import SessionLocal, init_db app = FastAPI() @app.on_event("startup") async def startup(): init_db() class Todo(BaseModel): task: str completed: bool def get_db(): db = SessionLocal() try: yield db finally: db.close() def to_dict(obj): return {column.name: getattr(obj, column.name) for column in obj.__table__.columns}
This will be the structure of our TODO objects. We have defined three fields: `task`, and `completed`.
app = FastAPI()
:
- This line creates an instance of the
FastAPI
class. This instance will serve as the web application, and will provide all of the functionality for defining endpoints, handling requests, and returning responses.
@app.on_event("startup")
:
- This line is a decorator that registers a function to be run when the FastAPI application starts up. It’s part of FastAPI’s event handling system.
async def startup():
:
- This line defines a coroutine named
startup
. This coroutine will be executed when the FastAPI application starts up, thanks to the@app.on_event("startup")
decorator above it.
init_db()
:
- Inside the
startup
coroutine, this line calls theinit_db
function defined earlier to initialize the database.
class Todo(BaseModel):
:
- This line defines a new class named
Todo
, which inherits fromBaseModel
(a class from the Pydantic library). This class will serve as the schema forTodo
objects throughout the application.
task: str
and completed: bool
:
- These lines define the fields of the
Todo
schema: atask
field of typestr
, and acompleted
field of typebool
.
def get_db():
:
- This line defines a function named
get_db
, which will be used to get a database session.
db = SessionLocal()
:
- Inside
get_db
, this line creates a new database session using theSessionLocal
session factory defined earlier.
try:
and finally:
:
- These lines establish a
try
/finally
block, which ensures that the code in thefinally
block will be executed no matter what, even if an exception occurs.
yield db
:
- Inside the
try
block, this line yields the database session to the caller. This is a way of providing the database session to other parts of the code while ensuring that thefinally
block will be executed to clean up the session afterwards.
db.close()
:
- Inside the
finally
block, this line closes the database session. This is important to prevent resource leaks by ensuring that every session is closed when it’s no longer needed.
def to_dict(obj):
:
- This line defines a function named
to_dict
, which will be used to convert SQLAlchemy ORM objects to dictionaries.
return {column.name: getattr(obj, column.name) for column in obj.__table__.columns}
:
- Inside
to_dict
, this line returns a dictionary that contains all of the data from the given ORM object. It does this by iterating over all of the columns of the object’s table, getting the value of each column from the object, and putting those values into a dictionary.
GET Request – Retrieving All TODOs
Now, let’s create our first endpoint for retrieving all TODOs. Add the following code to your `main.py` file:
@app.get("/todos", response_model=list[dict]) def read_todos(db: Session = Depends(get_db)): todos = db.query(TodoInDB).all() return [to_dict(todo) for todo in todos]
This endpoint will return all the TODOs stored in our application. We will define the `todos` variable later in the code.
@app.get("/todos", response_model=list[dict])
:
- This is a decorator provided by FastAPI to declare a new HTTP GET endpoint at the path
/todos
. Theresponse_model
parameter is a Pydantic model (or in this case, a Python type hint) that describes the shape of the response data. Here,list[dict]
indicates that the endpoint will return a list of dictionaries.
def read_todos(db: Session = Depends(get_db)):
:
- This line defines a new function
read_todos
which will handle requests to the/todos
endpoint. Thedb: Session = Depends(get_db)
argument instructs FastAPI to call theget_db
function (described in the previous code snippet) to obtain a database session, and to pass that session toread_todos
as thedb
argument.
todos = db.query(TodoInDB).all()
:
- Inside
read_todos
, this line creates a new SQLAlchemy query object that will query theTodoInDB
model. The.all()
method is then called on this query object to execute the query and return all rows from thetodos
table as a list ofTodoInDB
objects.
return [to_dict(todo) for todo in todos]
:
- This line is a list comprehension that iterates over each
TodoInDB
object in thetodos
list, calls theto_dict
function (described in the previous code snippet) to convert eachTodoInDB
object to a dictionary, and then returns the list of dictionaries. This list of dictionaries will be serialized to JSON and sent back to the client as the HTTP response.
POST Request – Creating a New TODO
To create a new TODO, we need to define another endpoint. Add the following code to your `main.py` file:
@app.post("/todos", response_model=dict, status_code=status.HTTP_201_CREATED) def create_todo(todo: Todo, db: Session = Depends(get_db)): db_todo = TodoInDB(**todo.model_dump()) db.add(db_todo) db.commit() db.refresh(db_todo) return to_dict(db_todo)
This endpoint expects a JSON object representing a TODO and adds it to our list of TODOs. We will define the `todos` list later in the code.
@app.post("/todos", response_model=dict, status_code=status.HTTP_201_CREATED)
:
- This line is a decorator provided by FastAPI to declare a new HTTP POST endpoint at the path
/todos
. Theresponse_model
parameter is a Pydantic model (or in this case, a Python type hint) that describes the shape of the response data. Here,dict
indicates that the endpoint will return a dictionary. Thestatus_code
parameter sets the HTTP status code to 201 (Created) for successful responses from this endpoint.
def create_todo(todo: Todo, db: Session = Depends(get_db)):
:
- This line defines a new function
create_todo
to handle requests to the/todos
endpoint. It accepts two arguments:todo
of typeTodo
(which is a Pydantic model you defined earlier), anddb
of typeSession
. FastAPI will call theget_db
function to obtain a database session and pass it as thedb
argument, as specified by theDepends
keyword.
db_todo = TodoInDB(**todo.model_dump())
:
- This line creates a new
TodoInDB
object from the data in thetodo
argument. It uses the**
(unpacking) operator to pass the fields of thetodo
object as keyword arguments to theTodoInDB
constructor, andmodel_dump()
to convert thetodo
object to a dictionary.
db.add(db_todo)
:
- This line adds the new
TodoInDB
object to the current database session, marking it as pending to be inserted into the database.
db.commit()
:
- This line commits the current database session, flushing all pending changes to the database. This will insert the new
TodoInDB
object into thetodos
table.
db.refresh(db_todo)
:
- This line refreshes the
db_todo
object with the current state in the database, ensuring it reflects any changes that occurred during the commit (such as the assignment of a newid
value by the database).
return to_dict(db_todo)
:
- This line calls the
to_dict
function (explained in a previous snippet) to convert thedb_todo
object to a dictionary, and returns this dictionary. This dictionary will be serialized to JSON and sent back to the client as the HTTP response.
PUT Request – Updating a TODO
Next, let’s create an endpoint for updating a TODO. Add the following code to your `main.py` file:
@app.put("/todos/{todo_id}", response_model=dict) def update_todo(todo_id: int, todo: Todo, db: Session = Depends(get_db)): db_todo = db.query(TodoInDB).filter(TodoInDB.id == todo_id).first() if db_todo is None: raise HTTPException(status_code=404, detail="Todo not found") for key, value in todo.model_dump().items(): setattr(db_todo, key, value) db.commit() db.refresh(db_todo) return to_dict(db_todo)
This endpoint expects an `id` parameter in the URL and a JSON object representing the updated TODO. It updates the TODO with the specified `id` if it exists in our list of TODOs.
@app.put("/todos/{todo_id}", response_model=dict)
:
- This line is a FastAPI decorator that defines a new HTTP PUT endpoint at the path
/todos/{todo_id}
. Theresponse_model
parameter indicates that the endpoint will return a dictionary. The{todo_id}
in the path is a path parameter that will be passed to theupdate_todo
function.
def update_todo(todo_id: int, todo: Todo, db: Session = Depends(get_db)):
:
- This line declares the
update_todo
function which will handle requests to the/todos/{todo_id}
endpoint. It accepts three arguments:todo_id
of typeint
,todo
of typeTodo
, anddb
of typeSession
. FastAPI will call theget_db
function to obtain a database session and pass it as thedb
argument, as specified by theDepends
keyword.
db_todo = db.query(TodoInDB).filter(TodoInDB.id == todo_id).first()
:
- This line queries the database for a
TodoInDB
object with the specifiedtodo_id
. It uses SQLAlchemy’s query interface to construct and execute the query.
if db_todo is None:
and raise HTTPException(status_code=404, detail="Todo not found")
:
- These lines check if the query returned a
TodoInDB
object. If not, it raises an HTTPException with a status code of 404 (Not Found) and a detail message of “Todo not found”.
for key, value in todo.model_dump().items():
and setattr(db_todo, key, value)
:
- These lines iterate over the fields of the
todo
object, updating the corresponding fields of thedb_todo
object with the new values. It uses themodel_dump()
method to convert thetodo
object to a dictionary, and thesetattr
function to update thedb_todo
object.
db.commit()
:
- This line commits the current database session, saving the changes to the
db_todo
object to the database.
db.refresh(db_todo)
:
- This line refreshes the
db_todo
object with the current state in the database, ensuring it reflects any changes that occurred during the commit.
return to_dict(db_todo)
:
- This line calls the
to_dict
function (explained in a previous snippet) to convert thedb_todo
object to a dictionary, and returns this dictionary. This dictionary will be serialized to JSON and sent back to the client as the HTTP response.
DELETE Request – Deleting a TODO
Finally, let’s create an endpoint for deleting a TODO. Add the following code to your `main.py` file:
@app.delete("/todos/{todo_id}", response_model=dict) def delete_todo(todo_id: int, db: Session = Depends(get_db)): db_todo = db.query(TodoInDB).filter(TodoInDB.id == todo_id).first() if db_todo is None: raise HTTPException(status_code=404, detail="Todo not found") db.delete(db_todo) db.commit() return {"message": "Todo deleted successfully"}
This endpoint expects an `id` parameter in the URL and deletes the TODO with the specified `id` if it exists in our list of TODOs.
@app.delete("/todos/{todo_id}", response_model=dict)
:
- This line is a FastAPI decorator that defines a new HTTP DELETE endpoint at the path
/todos/{todo_id}
. Theresponse_model
parameter indicates that the endpoint will return a dictionary. The{todo_id}
in the path is a path parameter that will be passed to thedelete_todo
function.
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
:
- This line declares the
delete_todo
function which will handle requests to the/todos/{todo_id}
endpoint. It accepts two arguments:todo_id
of typeint
, anddb
of typeSession
. FastAPI will call theget_db
function to obtain a database session and pass it as thedb
argument, as specified by theDepends
keyword.
db_todo = db.query(TodoInDB).filter(TodoInDB.id == todo_id).first()
:
- This line queries the database for a
TodoInDB
object with the specifiedtodo_id
. It uses SQLAlchemy’s query interface to construct and execute the query.
if db_todo is None:
and raise HTTPException(status_code=404, detail="Todo not found")
:
- These lines check if the query returned a
TodoInDB
object. If not, it raises an HTTPException with a status code of 404 (Not Found) and a detail message of “Todo not found”.
db.delete(db_todo)
:
- This line marks the
db_todo
object for deletion in the current database session.
db.commit()
:
- This line commits the current database session, which will delete the
db_todo
object from the database.
return {"message": "Todo deleted successfully"}
:
- This line returns a dictionary with a message indicating that the todo item was successfully deleted. This dictionary will be serialized to JSON and sent back to the client as the HTTP response.
Running the FastAPI Application
Now that you have created your FastAPI application and defined your models, endpoints, and database configuration, it’s time to run your application and interact with it.
1. Start the FastAPI Application:
- Navigate to the directory containing your FastAPI application file (e.g.,
main.py
). - Run the following command to start your FastAPI application:
uvicorn main:app --reload --host 0.0.0.0 --port 8000
main:app
tells Uvicorn to look for an application instanceapp
in a filemain.py
.--reload
tells Uvicorn to reload the application whenever the code is changed. This is useful during development but should be omitted in a production environment.--host 0.0.0.0
tells Uvicorn to listen on all public IPs of your server.--port 8000
specifies the port on which your application will be hosted.
2. Interact with your application:
Below are examples of curl
requests for each endpoint in your FastAPI application. Before running these commands, make sure your FastAPI application is running and accessible at http://localhost:8000
.
Creating a new Todo item:
curl -X POST "http://localhost:8000/todos" \ -H "accept: application/json" \ -H "Content-Type: application/json" \ -d '{"task":"Buy groceries","completed":false}'
Response:
{ "id": 1, "task": "Buy groceries", "completed": false }
Retrieving all Todo items:
$ curl -X GET "http://localhost:8000/todos" -H "accept: application/json"
Response (assuming the Todo item created in step 1 is the only item):
[ { "id": 1, "task": "Buy groceries", "completed": false } ]
Updating a Todo item (assuming todo_id
is 1 and you want to mark it as completed):
$ curl -X PUT "http://localhost:8000/todos/1" \ -H "accept: application/json" \ -H "Content-Type: application/json" \ -d '{"task":"Buy groceries","completed":true}'
Response:
{ "id": 1, "task": "Buy groceries", "completed": true }
Deleting a Todo item (assuming todo_id
is 1):
$ curl -X DELETE "http://localhost:8000/todos/1" -H "accept: application/json"
Response:
{ "message": "Todo deleted successfully" }
- FastAPI also provides an interactive API documentation interface:
- Open your web browser and navigate to
http://127.0.0.1:8000/docs
. - Here you can see all the available endpoints of your API, try them out, and see their responses.
- Open your web browser and navigate to
3. Stop your application:
When you want to stop your FastAPI application, simply press CTRL + C
in the terminal where Uvicorn is running.
4. Check Application Logs:
While your application is running, Uvicorn will display logs in the terminal. These logs provide useful information about the requests being made to your application and any errors that occur.
Generating Documentation
FastAPI provides built-in support for generating documentation using the OpenAPI standard. To access the documentation, run your FastAPI application and navigate to `http://localhost:8000/docs` in your web browser. You will see an interactive documentation page where you can test your endpoints and view detailed information about each endpoint.
Conclusion
Congratulations! You have successfully built a scalable and secure web application with Python and FastAPI. You have learned how to create all endpoints for a TODO application, set up documentation, and create automated tests. FastAPI’s simplicity and performance make it an excellent choice for building robust web applications. Keep exploring FastAPI’s features and continue building amazing applications!