Build a Task Manager with FastAPI
Build a full-stack REST API task manager with FastAPI and SQLite.
Introduction
30 Second Summary
Everyone has a to-do list, whether it's sticky notes on a monitor or an app on your phone. Behind every task management app is a web server that stores your tasks and sends them to your screen.
In this project, you will build a full-stack Task Manager application using Python. You'll create a REST API that stores tasks in a database and a web frontend that lets you add, complete, and delete tasks through your browser.
What You'll Build
When you're done, you'll open your app in the browser, add a task, click it to mark it complete with a satisfying strikethrough, and share a professional API docs page that lets anyone test your endpoints live.
By the end of this project, you'll have:
- A working REST API built with FastAPI where you can create, read, update, and delete tasks that persist in a SQLite database.
- A web frontend served by your API where you can type in a task, watch it appear instantly, click to toggle it complete, and hit delete to remove it.
- Auto-generated Swagger UI documentation at /docs that lets anyone interact with your API without writing a line of code.
- Secret Mission: Add filtering so users can view only pending tasks, only completed tasks, or all tasks.
Are there any prerequisites?
You should be comfortable with basic Python concepts like variables, functions, loops, and dictionaries. No web development experience is required.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Project
To build a full-stack task manager, you need a clean project structure with the right tools installed.
In this step, you will create a project folder, set up a Python virtual environment, and install FastAPI (your web framework) and SQLModel (your database library). Then you will confirm everything works by running a Hello World server.
In this step, get ready to:
- Create a project folder with a virtual environment.
- Install FastAPI and SQLModel.
- Create and run a Hello World API.
Create a project folder with a virtual environment
A virtual environment is an isolated Python workspace. It keeps the packages you install for this project separate from your system Python, so nothing conflicts.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir task-manager
cd task-manager
- Confirm the folder was created by running:
ls
You should see an empty output because the folder is brand new.
- Create a virtual environment by running:
python -m venv venv
- Confirm the virtual environment was created by running:
ls
You should see a venv folder listed in the output.
- Activate the virtual environment by running:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
Your terminal prompt now shows (venv) at the beginning. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then try activating again.
On macOS, make sure you typed source before the path. Without it, the command does nothing.
help me activate my Python virtual environment
- Now open VS Code on your project folder. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the task-manager folder, then click Open.
Why use VS Code for the rest of the project?
VS Code has a built-in terminal that opens directly inside your project folder. This means you can edit files and run commands in the same window without switching between apps.
Install FastAPI and SQLModel
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
Is the virtual environment active in VS Code's terminal?
VS Code often detects and activates the venv folder automatically. Look for (venv) at the start of your prompt. If you do not see it, run the activation command again from the platform tabs above.
- Install FastAPI with its standard extras (which includes the Uvicorn server) by running:
pip install "fastapi[standard]"
You should see download progress and a final line that includes Successfully installed followed by a list of package names.
- Install SQLModel by running:
pip install sqlmodel
You should see another Successfully installed confirmation.
Seeing a permission error or 'pip not found'?
Make sure your virtual environment is active. The (venv) prefix must appear in your terminal prompt before running pip commands.
If you see a warning about pip being outdated, you can safely ignore it. The installs still succeed.
help me fix pip install errors in my virtual environment
Create a Hello World route and run it
FastAPI apps start with a single Python file that defines your web server and its routes. A route is a URL path that your server responds to.
- In the VS Code Explorer sidebar, click the New File icon next to TASK-MANAGER.
- Name the file main.py and press Enter.
- Add the following code to main.py:
from fastapi import FastAPI
# Create the FastAPI application instance
app = FastAPI()
# Define a route that responds to GET requests at the root URL
@app.get("/")
def read_root():
return {"message": "Hello World"}
What does this code do?
from fastapi import FastAPI imports the framework you just installed.
app = FastAPI() creates your web application. This single object handles all incoming requests.
@app.get("/") is a decorator that tells FastAPI to run the function below it whenever someone visits the root URL (/) with a GET request. The function returns a Python dictionary, which FastAPI automatically converts to JSON.
- Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Before you run the server, what do you think you will see when you visit http://127.0.0.1:8000 in your browser?
- Start the development server by running:
fastapi dev main.py
Your terminal fills with output. Look for this line near the bottom:
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
What does this command do?
fastapi dev launches a development server powered by Uvicorn. It automatically detects the app object in main.py and starts serving it.
The dev mode enables auto-reload. Whenever you save changes to your code, the server restarts automatically so you always see the latest version.
- Open your browser and visit http://127.0.0.1:8000.
You should see the JSON response:
{"message":"Hello World"}
- Now visit http://127.0.0.1:8000/docs in your browser.
You should see Swagger UI, an interactive API documentation page that FastAPI generates automatically from your code. It lists your GET route and lets you test it directly from the browser.
What is Swagger UI?
Swagger UI is auto-generated documentation for your REST API. FastAPI creates it by reading your route definitions and data models. You did not write any extra code to get this page.
This is a major portfolio advantage. Anyone reviewing your project can open /docs and interact with your API without reading your source code.
Browser says 'connection refused'?
Make sure the server is still running in your terminal. If you accidentally pressed Ctrl+C, that stops the server. Run fastapi dev main.py again to restart it.
Double-check the URL is exactly http://127.0.0.1:8000 with no trailing slash after the port number.
help me fix my FastAPI server not loading in the browser
✔️ Awesome, I've got everything!
Great. Make sure you have saved main.py and your server is still running in the terminal.
ⓧ I'd like to double check the full code
Here is the complete main.py file for this step:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello World"}
Your development environment is ready and your first API route is live. Next up, you will build a real endpoint that accepts task data and stores it in a database.
Build Your First API Endpoint
Your Hello World route proves FastAPI is running and responding to requests. The next step is to build real endpoints that accept structured data, validate it automatically, and store it in a database.
In this step, you'll define a data model for tasks, connect a SQLite database, and create two API endpoints. FastAPI will automatically validate incoming requests and generate interactive documentation you can test in your browser.
In this step, get ready to:
- Define the Task data model with SQLModel.
- Create a POST endpoint to add tasks.
- Create a GET endpoint to list all tasks.
Define the Task data model with SQLModel
A data model defines the shape of your data. SQLModel lets you use the same Python class as both a database table definition and an API request/response schema.
- Switch back to VS Code with main.py open.
- Select all the existing code in main.py and delete it.
- Replace it with the following imports and model definitions:
from typing import Annotated
from fastapi import Depends, FastAPI, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
# Define the Task table model for the database
class Task(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
title: str = Field(index=True)
completed: bool = Field(default=False)
# Define the shape of data required to create a task
class TaskCreate(SQLModel):
title: str
What does this code do?
- Task is a database table model. The table=True argument tells SQLModel to create an actual SQL table from this class.
- id is auto-generated by the database, so it starts as None until a task is saved.
- TaskCreate defines what data the API expects when creating a task. It only requires a title because id and completed are handled automatically.
- Below the TaskCreate class, add the database engine setup and application initialization:
# Configure the SQLite database connection
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
# Create all tables defined in SQLModel
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
# Provide a database session for each request
def get_session():
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_session)]
app = FastAPI()
# Automatically create tables when the server starts
@app.on_event("startup")
def on_startup():
create_db_and_tables()
What does this code do?
- sqlite_url points to a local file called database.db that SQLite creates automatically.
- check_same_thread: False is required because FastAPI handles requests across multiple threads, but SQLite normally only allows the thread that created it to use it.
- get_session() provides a fresh database session for each request, then closes it automatically.
- SessionDep is a shorthand type alias so you don't repeat the dependency injection boilerplate in every endpoint.
- The on_startup event creates the database tables the moment the server starts.
- Save main.py.
- Check your terminal where the dev server is running. You should see it reload automatically with no errors.
- Visit http://127.0.0.1:8000/docs in your browser. The Swagger UI page loads, but the endpoint list is empty because you haven't defined any routes yet.
Seeing an error in the terminal?
- If you see an IndentationError, check that each class body and function body is indented with 4 spaces.
- If you see ModuleNotFoundError: No module named 'sqlmodel', make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
- If the server did not restart, confirm you saved the file.
Help me debug this error
Create the POST endpoint to add tasks
A POST request sends data to the server to create a new resource. Your endpoint will accept a task title and return the saved task with its auto-generated id.
- In main.py, add the following POST endpoint below the on_startup function:
@app.post("/tasks/", response_model=Task)
def create_task(task: TaskCreate, session: SessionDep):
# Convert the request data into a Task database object
db_task = Task.model_validate(task)
session.add(db_task)
session.commit()
session.refresh(db_task)
return db_task
What does this code do?
- response_model=Task tells FastAPI to format the response using the Task model, which includes the id field.
- Task.model_validate(task) converts the incoming TaskCreate data into a full Task database object.
- session.commit() saves the task to the database. session.refresh(db_task) reloads it so the auto-generated id is included in the response.
- Save main.py.
- Return to http://127.0.0.1:8000/docs in your browser and refresh the page.
- You should now see POST /tasks/ listed in the Swagger UI.
- Click on POST /tasks/ to expand it, then click Try it out.
- In the request body, replace the example JSON with:
{
"title": "Buy groceries"
}
- Click Execute.
- In the response below, you should see the task returned with an auto-generated id of 1 and completed set to false.
Not seeing POST /tasks/ in Swagger UI?
- Hard-refresh the page with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows). The browser may be caching the old docs page.
- Confirm the @app.post decorator appears AFTER the line app = FastAPI() in your file. Routes defined before the app exists will not register.
Help me fix my POST endpoint
Create the GET endpoint to list all tasks
Your API can create tasks, but you also need a way to retrieve them. A GET endpoint returns data without modifying anything on the server.
- In main.py, add the following GET endpoint below the create_task function:
@app.get("/tasks/", response_model=list[Task])
def read_tasks(
session: SessionDep,
offset: int = 0,
limit: Annotated[int, Query(le=100)] = 100,
):
# Fetch tasks from the database with pagination
tasks = session.exec(select(Task).offset(offset).limit(limit)).all()
return tasks
What does this code do?
- response_model=list[Task] tells FastAPI this endpoint returns a list of tasks.
- offset and limit are query parameters that enable pagination. By default, the endpoint returns up to 100 tasks starting from the first one.
- select(Task) builds a SQL query, and .all() executes it and returns every matching row as a Python list.
- Save main.py.
Before you test, think about what you expect to see. You created a task called "Buy groceries" in the previous substep. What should GET /tasks/ return?
- Refresh http://127.0.0.1:8000/docs in your browser.
- Click on GET /tasks/ to expand it, then click Try it out.
- Click Execute.
- You should see a JSON array containing your "Buy groceries" task with its id, title, and completed fields.
GET /tasks/ returning an empty list?
- If the server restarted between testing POST and GET, the database file may have been deleted. Check that database.db exists in your task-manager folder. If not, try creating a task again with POST.
- Confirm you saved main.py after adding the GET endpoint. Unsaved changes won't register with the dev server.
Help me debug my empty task list
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and both endpoints are working in the Swagger UI.
ⓧ I'd like to double check the full code
Your main.py should contain these sections in order. Scroll up and confirm each one is present:
- Imports from typing, fastapi, and sqlmodel.
- Task class (table model) and TaskCreate class (request model).
- SQLite engine configuration (sqlite_file_name, sqlite_url, connect_args, engine).
- create_db_and_tables() function and get_session() function.
- SessionDep type alias and app = FastAPI().
- on_startup event calling create_db_and_tables().
- POST /tasks/ endpoint (create_task function).
- GET /tasks/ endpoint (read_tasks function).
You now have a working API that can create and retrieve tasks from a real database. Next up, you'll add update and delete operations to complete the full set of CRUD actions.
Complete the CRUD API
Your API can create and list tasks, but a real task manager also needs to update and remove them.
In this step, you'll add the remaining CRUD operations: updating a task's fields with PATCH and removing a task with DELETE. You'll also learn a critical pattern for partial updates that prevents accidentally overwriting data with None values.
In this step, get ready to:
- Add a PATCH endpoint that safely updates only the fields a client sends.
- Add a DELETE endpoint that removes a task from the database.
- Add error handling that returns a 404 when a task does not exist.
Add the PATCH endpoint to update a task
Updating a task sounds simple, but there is a subtle problem. If a user only wants to mark a task as completed, you do not want to accidentally erase the task's title in the process. You need a separate model where every field is optional, so you can tell the difference between "the user sent this field" and "the user left this field out."
- In main.py, find this import line at the top of the file:
from fastapi import Depends, FastAPI, Query
- Replace it with this updated import that adds HTTPException:
from fastapi import Depends, FastAPI, HTTPException, Query
- Below your TaskCreate class, add the TaskUpdate model by pasting this code:
class TaskUpdate(SQLModel):
title: str | None = None
completed: bool | None = None
Why are both fields set to None?
Setting each field to None by default means the field is optional in the request body. If a client sends only {"completed": true}, the title field stays None (unset) rather than being required.
Later, model_dump(exclude_unset=True) uses this distinction to update only the fields the client actually provided, leaving the rest untouched in the database.
- At the bottom of main.py, below the read_tasks function, add the PATCH endpoint:
@app.patch("/tasks/{task_id}", response_model=Task)
def update_task(task_id: int, task: TaskUpdate, session: SessionDep):
# Look up the task in the database
task_db = session.get(Task, task_id)
if not task_db:
raise HTTPException(status_code=404, detail="Task not found")
# Only update the fields the client actually sent
task_data = task.model_dump(exclude_unset=True)
task_db.sqlmodel_update(task_data)
session.add(task_db)
session.commit()
session.refresh(task_db)
return task_db
What does this code do?
This endpoint receives a task ID from the URL and an optional set of fields from the request body.
- session.get(Task, task_id) fetches the existing task from the database, or returns None if it does not exist.
- task.model_dump(exclude_unset=True) creates a dictionary containing only the fields the client explicitly sent.
- task_db.sqlmodel_update(task_data) applies those changes to the database object without touching the other fields.
- Save main.py.
- Open http://127.0.0.1:8000/docs in your browser. You should see a new PATCH /tasks/{task_id} endpoint in the Swagger UI.
- Click PATCH /tasks/{task_id}, then click Try it out.
- Enter 1 for the task_id field (use the id of a task you created in the previous step).
- In the request body, type {"completed": true} and click Execute.
- You should see the task returned with "completed": true while the title remains unchanged.
Seeing a 404 response?
This means the task_id you entered does not exist in the database. Try calling GET /tasks/ first to see which IDs are available. If the list is empty, create a task with POST /tasks/ first.
help me debug a 404 error when calling PATCH /tasks/{task_id} in FastAPI
Add the DELETE endpoint
Now that you can update tasks, you need a way to remove them entirely.
- At the bottom of main.py, below the update_task function, add the DELETE endpoint:
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int, session: SessionDep):
# Look up the task to delete
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
session.delete(task)
session.commit()
return {"ok": True}
What does this code do?
- session.get(Task, task_id) fetches the task, returning None if it does not exist.
- session.delete(task) removes the task from the database.
- The endpoint returns {"ok": True} as a simple confirmation that the deletion succeeded.
- Save main.py.
- Return to the Swagger UI at http://127.0.0.1:8000/docs and refresh the page.
- Click DELETE /tasks/{task_id}, then click Try it out.
- Enter the id of a task you want to remove and click Execute.
- You should see {"ok": true} in the response body.
Response says Internal Server Error?
Check your terminal for the error traceback. The most common cause is a typo in the function. Make sure session.delete(task) uses the task variable (not task_id).
help me fix an Internal Server Error on my DELETE endpoint in FastAPI
Add error handling for missing tasks
Your PATCH and DELETE endpoints already handle missing tasks. Now you need a dedicated endpoint to fetch a single task by its id, which also returns a proper 404 error when the task does not exist.
- In main.py, find the read_tasks function (the GET /tasks/ endpoint). Directly below it, add this new endpoint to get a single task:
@app.get("/tasks/{task_id}", response_model=Task)
def read_task(task_id: int, session: SessionDep):
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
What does this code do?
This endpoint takes a task id from the URL path and looks it up in the database. If the task exists, it returns the full task object. If not, it raises an HTTPException with a 404 status code and a clear error message.
This same 404 pattern appears in your PATCH and DELETE endpoints too. It ensures your API always returns a meaningful error instead of crashing when a client requests a task that does not exist.
- Save main.py.
- Refresh the Swagger UI at http://127.0.0.1:8000/docs.
Before you test, what do you think will happen if you try to GET a task with an id of 9999?
- Click GET /tasks/{task_id}, then click Try it out.
- Enter 9999 for the task_id and click Execute.
- You should see a 404 response with {"detail": "Task not found"}.
- Now test with a valid id (try GET /tasks/ first to find one). You should see the full task object returned.
What are HTTP status codes?
A 404 status code means "Not Found." Your API uses it to tell the client that the task they requested does not exist. Other common codes include 200 (OK), 201 (Created), and 500 (Internal Server Error).
Returning proper status codes makes your API predictable. Frontend code can check the status to decide whether to show a success message or an error dialog.
Now run the full verification. In the Swagger UI, update a task's completed status with PATCH /tasks/{task_id}, then delete a different task with DELETE /tasks/{task_id}, and confirm the changes by calling GET /tasks/.
- The updated task should show its new completed value.
- The deleted task should no longer appear in the list.
Changes not showing in GET /tasks/?
Make sure you saved main.py after every edit. The dev server auto-reloads on save, but unsaved changes are invisible to the running server.
Also confirm you are testing with task IDs that actually exist. Call GET /tasks/ to see the current list and use an id from that response.
help me verify my CRUD endpoints are working correctly in FastAPI Swagger UI
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py and that all four endpoints (POST, GET list, GET single, PATCH, DELETE) appear in your Swagger UI.
ⓧ I'd like to double check the full code
Scroll up and confirm each of these pieces is present in your main.py, in order:
- The updated import line including HTTPException.
- The Task table model, TaskCreate model, and TaskUpdate model (in that order).
- The SQLite engine setup, create_db_and_tables, get_session, and SessionDep.
- The app = FastAPI() line and on_startup event.
- The POST /tasks/ endpoint (create_task).
- The GET /tasks/ endpoint (read_tasks).
- The GET /tasks/{task_id} endpoint (read_task).
- The PATCH /tasks/{task_id} endpoint (update_task).
- The DELETE /tasks/{task_id} endpoint (delete_task).
Your API now supports the full set of CRUD operations. Next up, you'll build a web frontend so users can interact with your task manager visually instead of through the Swagger docs.
Build the Web Frontend
Your API is fully functional, but right now it lives behind raw JSON responses and Swagger UI. Nobody wants to manage their tasks by typing JSON into a docs page.
In this step, you will build a web frontend that FastAPI serves directly. Users will see a clean interface with an input field and a button, all styled with CSS and ready to connect to your API.
In this step, get ready to:
- Create the HTML and CSS files for the task manager UI.
- Mount static files in FastAPI so the browser can load them.
- Serve the frontend from the root URL so visiting your app shows the page.
Create the static file structure
Web browsers need HTML, CSS, and JavaScript files to render a page. You will create a static folder in your project to hold these files.
- In your terminal (stop the running server with Ctrl+C if it is still running), create the static folder by running this command:
mkdir static
- Confirm the folder was created by running:
ls
You should see static listed alongside main.py and your venv folder.
Now create the HTML file that defines the page structure.
- Create a new file called index.html inside the static folder in VS Code, and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<h1>Task Manager</h1>
<div class="input-group">
<input type="text" id="task-input" placeholder="Enter a new task..." />
<button onclick="addTask()">Add Task</button>
</div>
<ul id="task-list"></ul>
</div>
<script src="/static/script.js"></script>
</body>
</html>
What does this HTML do?
The page has a simple structure: a container with a heading, an input field paired with an "Add Task" button, and an empty list where tasks will appear.
The link tag loads your stylesheet from /static/style.css, and the script tag loads JavaScript from /static/script.js (which you will create in the next step).
- Save static/index.html.
Next, create the stylesheet that will give your task manager a clean, modern look.
- Create a new file called style.css inside the static folder, and paste the base layout styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f5f5f5;
display: flex;
justify-content: center;
padding-top: 60px;
min-height: 100vh;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
height: fit-content;
}
h1 {
margin-bottom: 24px;
color: #333;
font-size: 24px;
}
- Below that code in the same file, add the input group styles:
.input-group {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.input-group input {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
outline: none;
}
.input-group input:focus {
border-color: #4a90d9;
}
.input-group button {
padding: 12px 20px;
background-color: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}
.input-group button:hover {
background-color: #357abd;
}
- Continue adding the task list styles below the input group code:
#task-list {
list-style: none;
}
#task-list li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
#task-list li:hover {
background-color: #f9f9f9;
}
#task-list li.completed span {
text-decoration: line-through;
color: #999;
}
- Finally, add the delete button styles at the bottom of the file:
.delete-btn {
background: none;
border: none;
color: #e74c3c;
font-size: 18px;
cursor: pointer;
padding: 4px 8px;
}
.delete-btn:hover {
color: #c0392b;
}
What do these styles do?
The CSS creates a centered white card on a light gray background. The input field and button sit side by side, and each task will display as a row with a delete button on the right.
The .completed class adds a strikethrough effect for finished tasks. You will not see any of this styling yet because FastAPI does not know about the static folder. The next substep connects everything.
- Save static/style.css.
- Confirm both files exist by running:
ls static
You should see index.html and style.css listed in the output.
Don't see both files?
Make sure you created both files inside the static folder, not in the project root. If they ended up in the wrong place, drag them into static/ using the VS Code sidebar.
Help me find where my static files ended up
Mount static files and serve the frontend
FastAPI does not serve files from disk by default. You need to explicitly mount the static folder and add a route that returns index.html when someone visits the root URL.
- In main.py, find the import lines at the top of the file:
from fastapi import Depends, FastAPI, HTTPException, Query
- Add these two new import lines directly below it:
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
- Next, find this line further down in the file:
app = FastAPI()
- Add the static files mount directly below it, so the two lines together look like this:
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
What does mounting do?
Mounting tells FastAPI to serve any file inside the static folder when the browser requests a URL starting with /static/. For example, /static/style.css maps to the file static/style.css on disk.
- Now find the on_startup function:
@app.on_event("startup")
def on_startup():
create_db_and_tables()
- Add this new route directly below the on_startup function (leave a blank line between them):
@app.get("/", include_in_schema=False)
def serve_frontend():
return FileResponse("static/index.html")
What does this route do?
This route catches any GET request to / (the root URL) and returns your HTML file. The include_in_schema=False argument hides this route from the Swagger docs since it is a frontend page, not an API endpoint.
- Save main.py.
Seeing an import error?
Make sure from fastapi.responses import FileResponse and from fastapi.staticfiles import StaticFiles are on their own lines, directly below the existing from fastapi import ... line.
Help me fix my FastAPI import error
Verify the frontend loads
Everything is wired up. Before you check the browser, take a moment to predict what you will see.
- Start the dev server by running:
fastapi dev main.py
Do you think the page will look styled, or will it appear as plain unstyled HTML?
- Open http://127.0.0.1:8000 in your browser.
You should see a styled task manager interface: a white card centered on a gray background, with a text input field, a blue "Add Task" button, and an empty list area below. The CSS is loading correctly because FastAPI is serving it from the mounted /static path.
Seeing a blank page or unstyled text?
If the page is completely blank, check that your serve_frontend route returns FileResponse("static/index.html") and that index.html is inside the static folder.
If the page shows unstyled text, the CSS is not loading. Confirm the app.mount line uses directory="static" and that style.css is saved inside the static folder.
Help me debug why my frontend page is not rendering correctly
✔️ Awesome, I've got everything!
Great. Double check you have saved both static/index.html, static/style.css, and main.py.
ⓧ I'd like to double check the full code
Your project should have these files at this point. Scroll up and confirm each section is present and matches.
- main.py: imports include FileResponse and StaticFiles. The app.mount line sits below app = FastAPI(). The serve_frontend route sits between on_startup and create_task.
- static/index.html: contains the full HTML with task-input, the Add Task button, and task-list ul.
- static/style.css: contains all four sections: base layout, input-group, task-list, and delete-btn styles.
Here is static/index.html for reference:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<h1>Task Manager</h1>
<div class="input-group">
<input type="text" id="task-input" placeholder="Enter a new task..." />
<button onclick="addTask()">Add Task</button>
</div>
<ul id="task-list"></ul>
</div>
<script src="/static/script.js"></script>
</body>
</html>
Here is the top of main.py showing the new imports, mount, and route:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Query
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from sqlmodel import Field, Session, SQLModel, create_engine, select
# ... models and engine unchanged ...
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.on_event("startup")
def on_startup():
create_db_and_tables()
@app.get("/", include_in_schema=False)
def serve_frontend():
return FileResponse("static/index.html")
# ... CRUD routes unchanged ...
Your frontend is live. Next up, you will write JavaScript that calls your API endpoints so the input field and button actually create, toggle, and delete tasks in real time.
Connect Frontend to API
Your frontend loads in the browser with a clean design, an input field, and an Add Task button. There is just one problem: none of it does anything.
The HTML page needs JavaScript to talk to your API endpoints. In this step, you will write the JavaScript that calls your REST API so users can create, complete, and delete tasks directly from the browser.
In this step, get ready to:
- Fetch and display tasks from the API when the page loads.
- Add new tasks from the input field.
- Toggle task completion and delete tasks with a click.
Write JavaScript to fetch and display tasks
Your HTML already includes a <script src="/static/script.js"> tag, but the file does not exist yet. The first function you need is one that fetches all tasks from your API and renders them as list items.
- In VS Code, right-click the static folder in the Explorer sidebar and select New File.
- Name the file script.js.
- Add the following code to static/script.js:
// Fetch all tasks from the API and render them in the list
async function loadTasks() {
const response = await fetch("/tasks/");
const tasks = await response.json();
const taskList = document.getElementById("task-list");
taskList.innerHTML = "";
tasks.forEach(function (task) {
const li = document.createElement("li");
li.className = task.completed ? "completed" : "";
const span = document.createElement("span");
span.textContent = task.title;
// Click the task title to toggle its completed status
span.onclick = function () {
toggleTask(task.id, task.completed);
};
const deleteBtn = document.createElement("button");
deleteBtn.className = "delete-btn";
deleteBtn.textContent = "\u00D7";
deleteBtn.onclick = function (e) {
e.stopPropagation();
deleteTask(task.id);
};
li.appendChild(span);
li.appendChild(deleteBtn);
taskList.appendChild(li);
});
}
// Load tasks immediately when the page opens
loadTasks();
What does this code do?
The loadTasks() function calls your GET /tasks/ endpoint, then loops through the response to build an HTML list item for each task.
- Each task gets a <span> for its title and a delete button (the multiplication sign character).
- The completed CSS class is applied when a task is done, which triggers the strikethrough styling you wrote earlier.
- The final loadTasks(); call at the bottom runs the function as soon as the page loads.
- Save static/script.js.
- Refresh http://127.0.0.1:8000 in your browser.
If you created any tasks through Swagger UI earlier, you should see them listed now. If not, the list is empty. Either way, the page loads without errors.
Seeing a blank page or console errors?
- Open your browser's DevTools (F12) and check the Console tab. A 404 error for script.js means the file is not in the static folder.
- Make sure the filename is exactly script.js (not scripts.js or script.JavaScript).
- help me debug why my script.js is not loading
Add task creation from the frontend
The page can display tasks, but you cannot add new ones from the browser yet. The HTML button already calls addTask() on click. You need to define that function.
- In static/script.js, find the loadTasks(); call at the very bottom of the file.
- Add the following code above that line:
// Read the input field, send a new task to the API, then refresh the list
async function addTask() {
const input = document.getElementById("task-input");
const title = input.value.trim();
if (!title) return;
await fetch("/tasks/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: title }),
});
input.value = "";
loadTasks();
}
// Allow pressing Enter to add a task
document.getElementById("task-input").addEventListener("keypress", function (e) {
if (e.key === "Enter") {
addTask();
}
});
What does this code do?
- addTask() grabs the text from the input field, sends a POST request to /tasks/ with the title as JSON, then clears the input and re-fetches the task list.
- The keypress listener lets the user press Enter to add a task instead of clicking the button every time.
- Save static/script.js and refresh your browser.
- Type a task name (like Buy groceries) in the input field and press Enter or click Add Task.
You should see your new task appear in the list below the input field. Add a couple more tasks to confirm it works consistently.
Task not appearing after clicking Add Task?
- Open DevTools (F12) and check the Network tab. You should see a POST request to /tasks/ with a 200 status. If you see a 422 error, your JSON body does not match the expected format.
- Make sure your addTask() function is placed above the loadTasks(); call at the bottom, not inside the loadTasks function.
- help me debug why addTask is not sending data to my API
Add toggle-complete and delete functionality
You can add tasks, but clicking them does nothing yet. The loadTasks() function already wires up click handlers that call toggleTask() and deleteTask(). You need to define those two functions.
- In static/script.js, find the document.getElementById("task-input").addEventListener line near the bottom.
- Add the following code above that listener:
// Send a PATCH request to flip the task's completed status
async function toggleTask(id, currentStatus) {
await fetch("/tasks/" + id, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ completed: !currentStatus }),
});
loadTasks();
}
// Send a DELETE request to remove the task entirely
async function deleteTask(id) {
await fetch("/tasks/" + id, {
method: "DELETE",
});
loadTasks();
}
What does this code do?
- toggleTask() sends a PATCH request with the opposite of the current completed value. Your API updates only that field (thanks to the TaskUpdate model and exclude_unset=True pattern from the previous step).
- deleteTask() sends a DELETE request to remove the task from the database. Both functions call loadTasks() afterwards to refresh the list.
- Save static/script.js.
Before you refresh, think about what you expect to happen when you click a task's title. What about clicking the X button?
- Refresh your browser at http://127.0.0.1:8000.
- Click a task's title to toggle it. The text should get a strikethrough and turn grey.
- Click the same task again to mark it as pending. The strikethrough disappears.
- Click the X button on a task to delete it. The task disappears from the list.
Clicking a task does nothing?
- Check the Console tab in DevTools. A ReferenceError: toggleTask is not defined means the function is not in the right place. It must be defined above the loadTasks(); call at the bottom.
- If clicking does nothing at all (no error, no change), make sure you saved the file and did a hard refresh (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to clear cached JavaScript.
- help me debug why toggleTask and deleteTask are not working
✔️ Awesome, I've got everything!
Great. Double check you have saved static/script.js and that adding, toggling, and deleting all work in the browser.
ⓧ I'd like to double check the full code
Your static/script.js should contain these sections in order. Scroll up and confirm each is present:
- The loadTasks() function (fetches tasks and renders them as list items).
- The addTask() function (reads input, POSTs to the API, clears input, refreshes list).
- The toggleTask(id, currentStatus) function (PATCHes the completed field).
- The deleteTask(id) function (DELETEs the task).
- The keypress event listener on task-input (calls addTask() on Enter).
- The standalone loadTasks(); call at the very end (triggers the initial load).
You now have a fully working full-stack application. Your browser talks to your API, your API talks to the database, and everything works together in a single Python project. Next up, you will take on a secret mission to add task filtering.
Secret mission
Add Task Filtering
Your task manager shows every task at once. Real applications let users filter data by status. Can you add filter buttons that show only pending tasks, only completed tasks, or all tasks at once?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- task-manager project folder (contains all source files, virtual environment, and SQLite database).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with your Task Manager.
- Your task-manager folder stays on your Desktop with all files intact.
- The FastAPI dev server is not running unless you start it manually, so nothing consumes resources in the background.
- Your SQLite database retains all tasks you created during the project.
Pause - I'll come back to this later
Stop the dev server to free up your terminal. Your files and database stay on disk ready for next time.
- If the FastAPI dev server is still running, press Ctrl+C in the terminal to stop it.
- Deactivate your virtual environment by running:
deactivate
When you come back, reactivate the virtual environment and start the server again.
- On macOS/Linux, run source venv/bin/activate from inside the task-manager folder.
- On Windows, run venv\Scripts\activate from inside the task-manager folder.
- Then run fastapi dev main.py to start the server.
Delete - I don't want to use this again
Remove all project files, the virtual environment, and the database. This cannot be undone.
- If the FastAPI dev server is still running, press Ctrl+C in the terminal to stop it.
- Deactivate your virtual environment by running:
deactivate
- Navigate to your Desktop and delete the entire project folder.
On macOS/Linux, run:
cd ~/Desktop
rm -rf task-manager
On Windows, run:
cd ~\Desktop
rmdir /s /q task-manager
This removes all source files, the virtual environment (including FastAPI and SQLModel), and the SQLite database.
Nice Work!
Nice Work!
Nice work! You just built a full-stack web application from scratch using Python.
You've learned how to:
- Built a REST API with FastAPI that handles full CRUD operations for creating, reading, updating, and deleting tasks.
- Connected a SQLite database using SQLModel to persist task data across server restarts.
- Served a web frontend (HTML/CSS/JavaScript) directly from FastAPI using static files and auto-generated interactive API documentation with Swagger UI.
- Secret Mission: Added query parameter filtering to let users view tasks by status.
Ready to quiz yourself?