Build a Persistent Task API
Build a FastAPI task API with SQLite persistence and validated REST endpoints.
Introduction
30 Second Summary
A task list feels reliable until closing it wipes out everything you entered. Useful tools need to remember your work when you return.
In this project, you will build a local task API with FastAPI and SQLite. Its interactive documentation lets you create validated tasks that remain available after the server restarts.
What You'll Build
Picture restarting your task API and seeing the same saved task return in your browser.
By the end of this project, you'll have:
- A documented REST API where you can create, list, retrieve, and update tasks through interactive documentation.
- Clear request validation powered by Pydantic, with structured feedback for invalid titles and missing tasks.
- A repository-ready project that keeps task data across server restarts and explains how to run every route locally.
- A Secret Mission: add a validated completion-status filter that returns only completed or incomplete tasks.
Are there any prerequisites?
You should be comfortable writing small Python programs. A Windows computer with Python and Visual Studio Code installed is enough to begin without previous backend or SQL experience.
Before We Start
A focused backend project proves more than a collection of disconnected framework features. This is your moment to commit to building a persistent task API that demonstrates one complete engineering loop.
Set Up a Reproducible Windows Project
A portfolio API needs a repeatable environment before its behavior can be trusted. An isolated setup prevents packages from another project from changing this one.
This step prepares your Windows workspace for the persistent task API. You will install pinned versions of FastAPI and Pydantic inside the project.
In this step, get ready to:
- Open the fastapi-task-api workspace with a supported Python version.
- Create requirements.txt with pinned dependencies.
- Activate .venv before installing the packages.
Create the project and check Python
A Visual Studio Code workspace keeps the files and terminal rooted in one named folder. This gives every later command the same starting point.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search box.
- Select the Visual Studio Code result.
- Click File in the top menu.
- Select Open Folder....
- Select Desktop in the Windows folder picker.
- Select New Folder.
- Enter fastapi-task-api as the folder name.
- Press Enter to create the folder.
- Click Select Folder.
- Select Yes, I trust the authors if the Workspace Trust prompt appears.
You should see fastapi-task-api at the top of the Explorer sidebar. That confirms Visual Studio Code is using the correct workspace.
- Click View in the top menu.
- Select Terminal.
- Select the Launch Profile dropdown in the terminal panel.
- Select Command Prompt.
Your terminal prompt should include fastapi-task-api. This confirms that commands run inside the project folder.
- Check your installed Python version by running this command:
python --version
What does this command do?
The command prints the Python version available to your Windows terminal. FastAPI requires Python 3.10 or newer for this project.
✔️ I see a supported version
Your output reports Python 3.10 or newer. Your interpreter is ready for the pinned dependencies.
ⓧ I see an older version
Your current Python version cannot run this FastAPI release. Upgrade Python before creating the virtual environment.
- Visit the Python downloads page.
- Install the current Python Install Manager for Windows.
- Restart Visual Studio Code so its terminal can detect the updated Python installation.
- Repeat the version check above until it reports Python 3.10 or newer.
ⓧ Command not found
Windows cannot currently find a Python installation from this terminal. Install Python before continuing.
- Visit the Python downloads page.
- Install the current Python Install Manager for Windows.
- Restart Visual Studio Code so its terminal can detect Python.
- Repeat the version check above until it reports Python 3.10 or newer.
That is the first compatibility check cleared. Your project now has a supported Python interpreter in the correct Windows workspace.
Pin dependencies and create the environment
A dependency file records the exact package releases this project expects. A virtual environment keeps those packages isolated inside fastapi-task-api.
- Select the Explorer view in the left Activity Bar.
- Select the New File... button.
- Enter requirements.txt as the file name.
- Press Enter to create the file.
- Add the exact dependencies by pasting this code into requirements.txt:
fastapi[standard]==0.141.1
pydantic==2.13.5
What does this file control?
- The first line pins FastAPI to 0.141.1.
- The standard extra supplies the FastAPI development command used in the next step.
- The second line pins Pydantic to 2.13.5.
- Save requirements.txt with Ctrl+S.
- Confirm requirements.txt appears under fastapi-task-api in the Explorer sidebar.
Cannot find requirements.txt?
Check that the Explorer heading shows fastapi-task-api. Reopen that folder if the file was created somewhere else.
Make sure the filename ends with .txt only.
Ask for help with the dependency file.
✔️ Awesome, I've got everything!
Your pinned dependency file is complete. Both packages now have one reproducible source of truth.
ⓧ I'd like to double check the full code
fastapi[standard]==0.141.1
pydantic==2.13.5
What should match?
Your complete requirements.txt file should contain these two lines in this order. Each line locks one dependency to the project version.
- Create the local virtual environment by running this command:
py -m venv .venv
What does this command do?
The Python launcher creates an isolated environment named .venv inside fastapi-task-api. Packages installed after activation stay scoped to this project.
- Confirm the .venv directory appears in the Explorer sidebar.
No .venv directory?
Confirm the terminal prompt includes fastapi-task-api. A different location creates the environment outside this project.
Ask for help creating the virtual environment.
- Activate the virtual environment by running this command:
.venv\Scripts\activate
What does activation change?
Activation points Python package commands at the environment inside this project. The environment name appears at the start of your terminal prompt as a visible confirmation.
- Confirm the environment name appears at the start of the Command Prompt line.
Environment name missing?
Check that the terminal profile is Command Prompt. The activation command in this step is written for that Windows shell.
Ask for help activating the environment.
Install and verify the pinned packages
The activated environment now gives the dependency installation a controlled destination. Installing from requirements.txt keeps your local setup aligned with the project file.
- Install the pinned project dependencies by running this command:
py -m pip install -r requirements.txt
What does this command install?
The command asks pip to read requirements.txt. It installs the two pinned packages plus the standard dependencies required by FastAPI.
- Wait for the Command Prompt input line to return.
- Confirm the final installation output lists installed packages without a failure message.
Package installation failed?
Confirm the environment name still appears at the start of the prompt. Check that both lines in requirements.txt match the pinned versions exactly.
Ask for help with the installation.
Before the final check, do you expect both package paths to point inside .venv? Pause for a moment.
- Verify the installed package versions and locations by running this command:
py -m pip show fastapi pydantic
What should the package details show?
FastAPI should report version 0.141.1. Pydantic should report version 2.13.5.
Each package path should contain .venv. That path proves the packages belong to this project environment.
- Confirm the output reports FastAPI version 0.141.1.
- Confirm the output reports Pydantic version 2.13.5.
- Confirm each displayed package path contains .venv.
Versions or paths do not match?
Activate the project environment again if the package path points somewhere outside .venv. Repeat the dependency installation if either version differs from the pinned file.
Ask for help interpreting the package details.
Your isolated Windows environment is ready with reproducible dependency versions. Next up, you will launch the first visible routes in your task API.
Launch the First Task API
Your isolated Windows environment is ready. The pinned packages now run from .venv.
A backend becomes tangible when a request reaches a route. A visible JSON response from a tiny in-memory FastAPI API proves the routing loop before persistence adds complexity.
In this step, get ready to:
- Create main.py with a FastAPI application backed by a temporary sample task.
- Launch the local development server.
- Test both API routes through the interactive documentation.
Create the in-memory API foundation
The app object owns every route. The temporary tasks list gives the first route something concrete to return.
- In the Explorer sidebar in Visual Studio Code, click New File.
- Enter main.py as the file name.
- Double-click requirements.txt in the Explorer sidebar to keep it open in a tab.
- Return to the main.py tab.
- Build the API foundation by pasting this code into main.py:
from fastapi import FastAPI
app = FastAPI(title="Task API", version="1.0.0")
tasks = [
{
"id": 1,
"title": "Try the task API",
"completed": False,
}
]
What does this code do?
- The FastAPI import makes the application class available.
- The app object names the API Task API.
- The application version is 1.0.0.
- The tasks list acts as temporary storage for one sample task.
- Save main.py from Visual Studio Code's File menu.
- Switch back to the Windows Command Prompt terminal from the previous step.
- Start the development server by running this command:
fastapi dev main.py
What does this command do?
This command starts FastAPI in local development mode. It loads the app object from main.py.
The terminal remains busy while the server is running. Keep it running while you edit the API.
You'll see the local server address plus the documentation URL in the terminal.
- Open http://127.0.0.1:8000/docs in your browser.
Your first FastAPI server is live. You'll see interactive documentation headed Task API.
Development server not starting?
- Confirm that the terminal still shows the virtual environment as active.
- Check that main.py is inside the fastapi-task-api folder.
- Compare the capitalization of FastAPI with the code above.
Help me diagnose why my FastAPI development server does not start.
Add the health route
A health route gives you a quick way to confirm that the API can receive a request. Its response proves that the routing loop reaches your Python function.
- Switch back to the main.py tab.
- Place your cursor below the closing ] of the tasks list.
- Add the health route by pasting this code:
@app.get("/health")
def health_check() -> dict[str, str]:
return {"status": "ok"}
How does this route work?
- The @app.get("/health") decorator maps an HTTP GET request for /health to the function below it.
- The health_check() function returns a Python dictionary.
- FastAPI converts that dictionary into a JSON response.
- Save main.py.
- Return to the browser tab containing the interactive documentation.
- Refresh http://127.0.0.1:8000/docs.
Before you send the health request, what response do you expect from this route?
- Select the GET /health operation to expand it.
- Activate the operation's request controls.
- Send the request from those controls.
You'll see {"status":"ok"} in a successful response body.
Health route missing or failing?
- Refresh the documentation after saving main.py.
- Check that @app.get("/health") begins at the left edge of the file.
- Check that return {"status": "ok"} is indented inside health_check().
Help me fix my FastAPI health route.
Add the task-list route
The second route exposes the temporary in-memory data. This gives you a visible task response before later steps add creation or persistence.
- Switch back to the main.py tab.
- Place your cursor below the health_check() function.
- Add the task-list route by pasting this code:
@app.get("/tasks")
def list_tasks() -> list[dict[str, object]]:
return tasks
How does task listing work?
- The @app.get("/tasks") decorator registers the task-listing route.
- The return annotation describes a list of dictionaries.
- The list_tasks() function returns the current contents of tasks.
- Save main.py.
- Return to the interactive documentation in your browser.
- Refresh the documentation page.
Before you send the task request, how many task objects do you expect the response to contain?
- Select the GET /tasks operation to expand it.
- Activate the operation's request controls.
- Send the request from those controls.
You'll see [{"id":1,"title":"Try the task API","completed":false}] in a successful response body.
Task response not appearing?
- Check that return tasks is indented inside list_tasks().
- Confirm that the tasks list appears above the route definitions.
- Refresh the documentation after every saved correction.
Help me fix my FastAPI task-list route.
✔️ Awesome, I've got everything!
- Save main.py.
ⓧ I'd like to double check the full code
from fastapi import FastAPI
app = FastAPI(title="Task API", version="1.0.0")
tasks = [
{
"id": 1,
"title": "Try the task API",
"completed": False,
}
]
@app.get("/health")
def health_check() -> dict[str, str]:
return {"status": "ok"}
@app.get("/tasks")
def list_tasks() -> list[dict[str, object]]:
return tasks
What should match?
- The app object carries the API metadata.
- The tasks list contains one sample task.
- The /health route returns the status response.
- The /tasks route returns the temporary task list.
Your API now answers two routes with JSON. Next, you'll add validated task creation before exposing what a restart does to in-memory data.
Validate New Tasks and Reveal Data Loss
Your health route and task list prove that FastAPI can route requests. The next test is whether the API can protect the shape of each task.
This step adds Pydantic models to validate titles. It also puts the temporary task list through a restart.
In this step, get ready to:
- Define validated request and response schemas.
- Expose task creation with an intentional 201 response.
- Test whether the temporary task list survives a server restart.
Define validated task schemas
A request schema sets the rules for incoming data. A response schema documents the fields that successful task responses contain.
- In main.py, replace the current from fastapi import FastAPI line with the import group below:
from typing import Any
from fastapi import FastAPI, status
from pydantic import BaseModel, Field
What Do These Imports Add?
- Any supports route annotations for dictionary-based task data.
- status provides named HTTP status constants.
- BaseModel provides the foundation for each validated schema.
- Field applies limits to the task title.
- Save main.py.
- Switch to the development server terminal to confirm the automatic reload completes.
You should see the server finish reloading without a Python traceback.
Seeing a Reload Error?
Check that all three import lines appear at the top of main.py. Make sure the original FastAPI-only import is gone.
Ask for help with the import error.
- In main.py, add the task schemas directly above the app = FastAPI(title="Task API", version="1.0.0") line by pasting:
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=100)
class Task(BaseModel):
id: int
title: str
completed: bool
What Do These Models Protect?
- TaskCreate describes the JSON accepted when someone creates a task.
- Field(min_length=1, max_length=100) requires a title containing between 1 and 100 characters.
- Task describes the ID, title, and completion state returned by the API.
- Save main.py.
- Return to the development server terminal to inspect the latest reload.
You should see the server complete another reload without a Python traceback.
Models Causing a Reload Failure?
Check the indentation beneath both class declarations. Confirm that TaskCreate uses BaseModel with the same capitalization.
Ask for help with the model definitions.
The response model connects the existing list route to the new Task schema. This makes the route contract visible in the interactive documentation.
- In main.py, replace the existing list_tasks() route with:
@app.get("/tasks", response_model=list[Task])
def list_tasks() -> list[dict[str, Any]]:
return tasks
What Does the Response Model Do?
- response_model=list[Task] declares that the route returns a list of task-shaped objects.
- list[dict[str, Any]] describes the dictionary-based value returned by the current in-memory implementation.
- Save main.py.
- Return to http://127.0.0.1:8000/docs.
- Refresh the interactive documentation.
- Execute GET /tasks.
You should see the original sample task. The route documentation now describes each returned item with the Task schema.
Task Schema Missing?
Confirm that the route decorator includes response_model=list[Task]. Refresh the documentation after the development server finishes reloading.
Ask for help with the response model.
Expose the validated create route
A create route receives a validated request body before it changes application state. A successful creation uses the named 201 status constant to make the outcome explicit.
- In main.py, add the create route directly below list_tasks() by pasting:
@app.post(
"/tasks",
response_model=Task,
status_code=status.HTTP_201_CREATED,
)
def create_task(task: TaskCreate) -> dict[str, Any]:
new_task = {
"id": len(tasks) + 1,
"title": task.title,
"completed": False,
}
tasks.append(new_task)
return new_task
What Does This Route Do?
- task: TaskCreate validates the incoming JSON before the function runs.
- len(tasks) + 1 generates the next ID from the current list length.
- tasks.append(new_task) stores the new task in the running Python process.
- status.HTTP_201_CREATED marks the response as a successful resource creation.
- Save main.py.
- Return to http://127.0.0.1:8000/docs.
- Refresh the interactive documentation.
- Expand POST /tasks.
- Enable editing for the request body.
- Clear the text between the quotation marks for title.
- Send the empty-title request.
You should see an automatic validation response explaining that the title is too short. The route function does not create a task from that invalid body.
- Prepare a valid request by replacing the request body with the JSON below:
{
"title": "Write API documentation"
}
Why Does This Request Pass?
The title value contains text within the configured length limits. Pydantic converts the JSON body into a validated TaskCreate object.
- Send the valid request.
You should see a 201 response containing ID 2. The returned task has the title Write API documentation with a completion state of false.
- Execute GET /tasks.
You should now see the original sample task plus the task you created. Your API can validate input and return a documented task response.
Valid Request Still Rejected?
Confirm that the request body contains a string named title. Check that the create route accepts task: TaskCreate.
Ask for help with the create request.
Use the tabs below to confirm that your completed main.py matches the validated in-memory API.
✔️ Awesome, I've got everything!
Your main.py now matches the validated in-memory API.
ⓧ I'd like to double check the full code
from typing import Any
from fastapi import FastAPI, status
from pydantic import BaseModel, Field
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=100)
class Task(BaseModel):
id: int
title: str
completed: bool
app = FastAPI(title="Task API", version="1.0.0")
tasks = [
{
"id": 1,
"title": "Try the task API",
"completed": False,
}
]
@app.get("/health")
def health_check() -> dict[str, str]:
return {"status": "ok"}
@app.get("/tasks", response_model=list[Task])
def list_tasks() -> list[dict[str, Any]]:
return tasks
@app.post(
"/tasks",
response_model=Task,
status_code=status.HTTP_201_CREATED,
)
def create_task(task: TaskCreate) -> dict[str, Any]:
new_task = {
"id": len(tasks) + 1,
"title": task.title,
"completed": False,
}
tasks.append(new_task)
return new_task
Restart the API to reveal data loss
The task list currently lives in process memory. Restarting the process tests whether the created task belongs to durable application state.
Before the restart, consider whether the new task will still appear in GET /tasks.
- Stop the running development server by pressing Ctrl+C.
- Restart the development server by running:
fastapi dev main.py
What Does This Restart Test?
The command starts a fresh Python process from main.py. The new process recreates every module-level value from the source file.
- Return to http://127.0.0.1:8000/docs.
- Refresh the interactive documentation.
- Execute GET /tasks.
You should see only the sample task with ID 1. The task titled Write API documentation has disappeared.
Server Did Not Restart?
Confirm that the terminal still shows the activated .venv environment. Make sure you run the command from the fastapi-task-api folder.
Ask for help restarting the API.
Why Did the Task Disappear?
The tasks list is rebuilt from the sample data whenever Python loads main.py. A list in process memory cannot preserve new tasks after that process stops.
You have a validated API plus a concrete storage failure to solve. Next, you will move task data into SQLite so created tasks survive server restarts.
Persist Tasks with SQLite
Your FastAPI API now validates requests. However, the last restart erased the task you created because the in-memory list disappeared with the server process.
SQLite stores those tasks in a file on disk. This step moves task creation and listing into a database layer so the same data survives every development-server restart.
In this step, get ready to:
- Create the SQLite connection and table initialization helpers.
- Add parameterized functions that create and list persistent tasks.
- Connect the API routes to SQLite and prove that a task survives a restart.
Create the SQLite database layer
A dedicated database module keeps storage details separate from HTTP routes. Each helper opens the local tasks.db file only when it needs a connection.
Why use SQLite directly?
Python includes the sqlite3 module. SQLite also runs without a separate database server.
Working with the module directly exposes the transferable parts of database code. You will practice parameter binding, row mapping, commits, and connection cleanup.
- In the Visual Studio Code Explorer sidebar from earlier, click the New File icon.
- Enter database.py as the file name.
- Press Enter to create the file.
- Add the database path and connection helper by pasting this code into database.py:
import sqlite3
from typing import Any
DATABASE_PATH = "tasks.db"
def get_connection() -> sqlite3.Connection:
connection = sqlite3.connect(DATABASE_PATH)
connection.row_factory = sqlite3.Row
return connection
What does this code do?
- The DATABASE_PATH constant gives every helper one location for the database file.
- The get_connection() function opens tasks.db. SQLite creates that file when the application first connects.
- The sqlite3.Row row factory makes each database row accessible through its column names.
- Save database.py.
- Check the new module for syntax errors by running this command in the activated Command Prompt terminal:
py -m py_compile database.py
What does this check prove?
The command compiles database.py into Python bytecode. A successful check returns you to the Command Prompt without an error.
Seeing a compilation error?
Compare the import names and indentation with the code block above. Python reports the file line where it could not understand the code.
Check that DATABASE_PATH uses matching quotation marks around tasks.db.
Ask for help with the exact line Python reported: Help me fix the compilation error in my database connection helper.
Database rows store the completion state as an integer. The API needs a normal Python boolean before it can validate the response against the Task model.
- Add the row-mapping helper below get_connection() in database.py by pasting this code:
def row_to_task(row: sqlite3.Row) -> dict[str, Any]:
return {
"id": row["id"],
"title": row["title"],
"completed": bool(row["completed"]),
}
How does row mapping help?
- The row_to_task() helper converts a database row into the dictionary shape expected by the API.
- Named column access keeps the mapping tied to id, title, and completed.
- The bool() conversion turns SQLite's stored integer into the response model's boolean value.
- Save database.py.
- Confirm that the mapper has valid Python syntax by running:
py -m py_compile database.py
What should happen?
The Command Prompt returns without an error when both helpers compile successfully. This keeps the feedback loop small before you add SQL.
Is the mapper failing to compile?
Check that every dictionary key has a matching closing quotation mark. Confirm that the closing brace aligns with return.
Use the reported line to narrow down the mismatch: Help me debug the row_to_task function in database.py.
The database also needs a table before any route can save a task. The initialization helper creates that table safely whenever the application starts.
- Add the table initialization helper below row_to_task() in database.py by pasting this code:
def initialize_database() -> None:
connection = get_connection()
try:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0
CHECK (completed IN (0, 1))
)
"""
)
connection.commit()
finally:
connection.close()
What does initialization guarantee?
- The CREATE TABLE IF NOT EXISTS statement prepares the tasks table while preserving an existing table.
- The primary key generates a new identifier for every inserted task.
- The completion column defaults to 0. Its check constraint only accepts 0 or 1.
- The explicit commit() saves the table creation. The finally block closes the connection even if the SQL operation fails.
- Save database.py.
- Compile the expanded database module by running:
py -m py_compile database.py
What does success look like?
The Command Prompt returns without an error when the connection, mapper, and initialization helpers all have valid syntax. The server will call the initialization helper after you wire it into main.py.
Seeing an error in the SQL block?
Check that the triple quotation marks surround the complete table statement. Confirm that connection.commit() remains inside the try block.
Share the reported line without changing the table design: Help me fix the initialize_database syntax or indentation.
Add persistent create and list functions
The API needs database operations that match its existing routes. Each write uses a transaction so the inserted task reaches disk before the connection closes.
- Add the persistent create function below initialize_database() in database.py by pasting this code:
def create_task_record(title: str) -> dict[str, Any]:
connection = get_connection()
try:
cursor = connection.execute(
"INSERT INTO tasks (title) VALUES (?)",
(title,),
)
connection.commit()
row = connection.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?",
(cursor.lastrowid,),
).fetchone()
if row is None:
raise RuntimeError("Task was created but could not be loaded")
return row_to_task(row)
finally:
connection.close()
How does task creation stay safe?
- The ? placeholders keep the task title separate from the SQL statement.
- The single-item tuple supplies the title as a bound value.
- The lastrowid value identifies the new row so the function can load the generated ID.
- The finally block closes the connection after either a successful return or an exception.
- Save database.py.
- Check the new write function by running:
py -m py_compile database.py
What should the compiler confirm?
A clean return to the Command Prompt confirms that the insert, commit, lookup, and cleanup paths form valid Python code. The route will exercise the database behavior after the API is connected.
Is the create function failing?
Check the comma after (title,). A one-item tuple requires that comma.
Confirm that fetchone() belongs to the second connection.execute() call.
Ask for a focused review of the failing lines: Help me debug create_task_record without changing its parameterized SQL.
The list route needs a fresh database query for every request. Ordering by ID makes the response predictable as more tasks are added.
- Add the persistent list function below create_task_record() in database.py by pasting this code:
def list_task_records() -> list[dict[str, Any]]:
connection = get_connection()
try:
rows = connection.execute(
"SELECT id, title, completed FROM tasks ORDER BY id"
).fetchall()
return [row_to_task(row) for row in rows]
finally:
connection.close()
How does listing work?
- The query loads every saved task in ID order.
- The fetchall() call returns all matching rows.
- The list comprehension passes each row through row_to_task() so the API receives dictionaries with boolean completion values.
- Save database.py.
- Compile the complete database module by running:
py -m py_compile database.py
What does this checkpoint prove?
The Command Prompt returns without an error when every database helper compiles together. Your storage layer is now ready for the API routes.
Does the list function fail to compile?
Check that fetchall() follows the closing parenthesis of connection.execute().
Confirm that the list comprehension uses the existing row_to_task() helper.
Get help with the exact compiler location: Help me fix list_task_records in database.py.
✔️ Awesome, I've got everything!
Great. Save database.py before connecting it to the API.
ⓧ I'd like to double check the full code
Compare your complete database.py file with this version.
import sqlite3
from typing import Any
DATABASE_PATH = "tasks.db"
def get_connection() -> sqlite3.Connection:
connection = sqlite3.connect(DATABASE_PATH)
connection.row_factory = sqlite3.Row
return connection
def row_to_task(row: sqlite3.Row) -> dict[str, Any]:
return {
"id": row["id"],
"title": row["title"],
"completed": bool(row["completed"]),
}
def initialize_database() -> None:
connection = get_connection()
try:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0
CHECK (completed IN (0, 1))
)
"""
)
connection.commit()
finally:
connection.close()
def create_task_record(title: str) -> dict[str, Any]:
connection = get_connection()
try:
cursor = connection.execute(
"INSERT INTO tasks (title) VALUES (?)",
(title,),
)
connection.commit()
row = connection.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?",
(cursor.lastrowid,),
).fetchone()
if row is None:
raise RuntimeError("Task was created but could not be loaded")
return row_to_task(row)
finally:
connection.close()
def list_task_records() -> list[dict[str, Any]]:
connection = get_connection()
try:
rows = connection.execute(
"SELECT id, title, completed FROM tasks ORDER BY id"
).fetchall()
return [row_to_task(row) for row in rows]
finally:
connection.close()
How should I compare this file?
Check the function order, indentation, SQL placeholders, commits, and finally blocks. These details keep the storage behavior consistent across every request.
Connect FastAPI to SQLite and prove persistence
The storage helpers are isolated in database.py. The final wiring makes FastAPI initialize the table during startup and call those helpers from the existing routes.
- Return to the Command Prompt terminal running the development server.
- Press Ctrl+C to stop the server before editing its startup behavior.
- Return to main.py in Visual Studio Code.
- Find this import section at the top of the file:
from typing import Any
from fastapi import FastAPI, status
from pydantic import BaseModel, Field
What is missing from the current imports?
The existing header only imports the API and validation dependencies. Startup initialization also needs the asynchronous context manager and the three database functions.
- Replace that import section with this expanded version:
from contextlib import asynccontextmanager
from typing import Any
from fastapi import FastAPI, status
from pydantic import BaseModel, Field
from database import create_task_record, initialize_database, list_task_records
What do these imports enable?
The asynccontextmanager decorator supports the application lifespan function. The database imports expose only the operations needed by main.py.
- Scroll below the Task model.
- Find the application declaration followed by the temporary task list:
app = FastAPI(title="Task API", version="1.0.0")
tasks = [
{
"id": 1,
"title": "Try the task API",
"completed": False,
}
]
Why does this block need to change?
The tasks list is the process memory that lost your previous task. The application declaration also lacks a startup hook for preparing the database table.
- Replace that application and list block with this lifespan configuration:
@asynccontextmanager
async def lifespan(app: FastAPI):
initialize_database()
yield
app = FastAPI(title="Task API", version="1.0.0", lifespan=lifespan)
How does the lifespan function help?
- FastAPI calls lifespan() when the application starts.
- The initialize_database() call creates the table before the first request reaches a task route.
- The yield line hands control back to the running application after startup work finishes.
- Save main.py.
- Compile the startup wiring by running:
py -m py_compile main.py
What should happen?
The Command Prompt returns without an error when the new imports and lifespan configuration compile. This confirms that main.py can load the database module.
Does main.py fail to import the database module?
Confirm that database.py sits beside main.py in the fastapi-task-api folder.
Check that the imported function names exactly match their definitions in database.py.
Ask for help with the import or lifespan line: Help me debug the database imports or lifespan function in main.py.
The two task routes still read from and write to the deleted in-memory list. Each route now needs to delegate its work to the matching database helper.
- Find the current list route in main.py.
@app.get("/tasks", response_model=list[Task])
def list_tasks() -> list[dict[str, Any]]:
return tasks
What does the current route depend on?
This version returns the old tasks list. That name no longer exists after the lifespan change removed in-memory storage.
- Replace the current list route with this database-backed version:
@app.get("/tasks", response_model=list[Task])
def list_tasks() -> Any:
return list_task_records()
How does the list route change?
The route preserves its URL and response model. Its return value now comes from the latest rows in tasks.db.
- Find the current create route below list_tasks().
@app.post(
"/tasks",
response_model=Task,
status_code=status.HTTP_201_CREATED,
)
def create_task(task: TaskCreate) -> dict[str, Any]:
new_task = {
"id": len(tasks) + 1,
"title": task.title,
"completed": False,
}
tasks.append(new_task)
return new_task
What does the current create route do?
The existing function generates an ID from the list length. It appends the new dictionary to process memory.
- Replace the current create route with this persistent version:
@app.post(
"/tasks",
response_model=Task,
status_code=status.HTTP_201_CREATED,
)
def create_task(task: TaskCreate) -> Any:
return create_task_record(task.title)
How does creation become persistent?
The route still receives a validated TaskCreate body. The create_task_record() helper inserts its title and returns the stored row.
- Save main.py.
- Compile the complete API module by running:
py -m py_compile main.py
What does this checkpoint prove?
A clean return to the Command Prompt confirms that both routes reference defined database helpers. The API is ready for a persistence test.
Seeing an undefined tasks error?
Search main.py for any remaining use of the old tasks variable. The list route should call list_task_records().
The create route should call create_task_record(task.title).
Ask for a comparison of the two route functions: Help me remove the remaining in-memory tasks references.
✔️ Awesome, I've got everything!
Your API now initializes SQLite during startup and delegates task storage to database.py.
ⓧ I'd like to double check the full code
Compare your complete main.py file with this version.
from contextlib import asynccontextmanager
from typing import Any
from fastapi import FastAPI, status
from pydantic import BaseModel, Field
from database import create_task_record, initialize_database, list_task_records
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=100)
class Task(BaseModel):
id: int
title: str
completed: bool
@asynccontextmanager
async def lifespan(app: FastAPI):
initialize_database()
yield
app = FastAPI(title="Task API", version="1.0.0", lifespan=lifespan)
@app.get("/health")
def health_check() -> dict[str, str]:
return {"status": "ok"}
@app.get("/tasks", response_model=list[Task])
def list_tasks() -> Any:
return list_task_records()
@app.post(
"/tasks",
response_model=Task,
status_code=status.HTTP_201_CREATED,
)
def create_task(task: TaskCreate) -> Any:
return create_task_record(task.title)
How should I compare this file?
Check the imports, lifespan placement, application declaration, and route return values. The existing models and route paths should remain unchanged.
Before you run the server, predict which file the lifespan function creates when the application starts.
- Start the database-backed development server by running:
fastapi dev main.py
What does startup do now?
The development command loads the application and enters its lifespan function. That startup call creates tasks.db with the tasks table before the API accepts requests.
Does the server stop during startup?
Check the terminal for the file and line connected to the failure. A database startup failure usually points to initialize_database() or its SQL block.
Confirm that the activated terminal still shows .venv in its prompt.
Ask for help using the startup traceback: Help me diagnose why FastAPI cannot initialize tasks.db.
- Open http://127.0.0.1:8000/docs in your browser.
- Expand the POST /tasks operation.
- Enable the request body editor.
- Replace the request body with this JSON:
{
"title": "Write API documentation"
}
What does this request test?
The request passes the title through the existing TaskCreate validation. The route then sends the validated title to the parameterized insert function.
- Submit the create request.
You will see a successful response containing a generated ID, the title Write API documentation, and a completion value of false.
- Return to the Command Prompt terminal.
- Press Ctrl+C to stop the development server.
Before you restart the server, predict whether the task response will still appear in the list.
- Restart the development server by running:
fastapi dev main.py
What changes after the restart?
The server reconnects to the existing tasks.db file. The table initialization preserves the rows already stored there.
- Refresh http://127.0.0.1:8000/docs in your browser.
- Expand the GET /tasks operation.
- Submit the list request.
You will see the task titled Write API documentation with its generated ID and false completion value. The restart no longer erases newly created work.
That is the persistence loop working. Your API can stop, start, and recover its saved tasks from SQLite.
Your task data now survives server restarts. Next, you will add individual task lookup, completion updates, and clear responses for missing records.
Complete the API and Repository
Your FastAPI service now keeps task data in SQLite across server restarts. A client still needs a way to retrieve one task or change its completion state.
This step completes the API contract with partial updates and clear HTTP errors. You will also add the files that explain the project to a repository reviewer.
In this step, get ready to:
- Add database helpers that retrieve tasks and update completion states.
- Expose task retrieval and partial updates through the API.
- Document the project and exclude generated files from version control.
Add lookup and update helpers
Resource-specific routes need database helpers that return one matching row. A missing row becomes a clear signal that the API can translate into a client error.
- Switch back to database.py in Visual Studio Code.
- Add the retrieval helper below list_task_records() by pasting this code:
def get_task_record(task_id: int) -> dict[str, Any] | None:
connection = get_connection()
try:
row = connection.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?",
(task_id,),
).fetchone()
return row_to_task(row) if row is not None else None
finally:
connection.close()
What does this helper do?
- The query selects the task whose ID matches task_id.
- The ? placeholder keeps the ID separate from the SQL statement.
- The helper returns a mapped task when a row exists.
- The helper returns None when no row matches.
- Save database.py.
- Watch the development-server terminal for the automatic reload.
You should see the server finish reloading without a Python error. This confirms that the retrieval helper loads successfully.
Does the server fail to reload?
Check that get_task_record() starts at the left edge of database.py. Confirm that the finally block aligns with try.
Use Help me fix my get_task_record function if the reload still fails.
A partial update changes only the completion column. The database must commit that write before it reloads the updated row.
- Add the completion-update helper below get_task_record() by pasting this code:
def update_task_record(task_id: int, completed: bool) -> dict[str, Any] | None:
connection = get_connection()
try:
cursor = connection.execute(
"UPDATE tasks SET completed = ? WHERE id = ?",
(int(completed), task_id),
)
connection.commit()
if cursor.rowcount == 0:
return None
row = connection.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?",
(task_id,),
).fetchone()
return row_to_task(row) if row is not None else None
finally:
connection.close()
How does the update stay safe?
- The helper converts the Boolean completion value into the integer stored by SQLite.
- The query binds the completion value and task ID through separate ? placeholders.
- The explicit commit() makes the update durable.
- A rowcount of zero signals that the requested task does not exist.
- Save database.py.
- Watch the development-server terminal for another automatic reload.
You should see the server resume without a Python error. Both database operations are now ready for API routes.
Seeing a database helper error?
Check that both SQL values sit inside (int(completed), task_id). Confirm that connection.commit() appears immediately after the update.
Use Help me debug my completion update helper if you need a second pair of eyes.
✔️ Awesome, I've got everything!
Your database layer can now retrieve one task and persist a completion update.
ⓧ I'd like to double check the full code
- Compare your complete database.py file with this reference:
import sqlite3
from typing import Any
DATABASE_PATH = "tasks.db"
def get_connection() -> sqlite3.Connection:
connection = sqlite3.connect(DATABASE_PATH)
connection.row_factory = sqlite3.Row
return connection
def row_to_task(row: sqlite3.Row) -> dict[str, Any]:
return {
"id": row["id"],
"title": row["title"],
"completed": bool(row["completed"]),
}
def initialize_database() -> None:
connection = get_connection()
try:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0
CHECK (completed IN (0, 1))
)
"""
)
connection.commit()
finally:
connection.close()
def create_task_record(title: str) -> dict[str, Any]:
connection = get_connection()
try:
cursor = connection.execute(
"INSERT INTO tasks (title) VALUES (?)",
(title,),
)
connection.commit()
row = connection.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?",
(cursor.lastrowid,),
).fetchone()
if row is None:
raise RuntimeError("Task was created but could not be loaded")
return row_to_task(row)
finally:
connection.close()
def list_task_records() -> list[dict[str, Any]]:
connection = get_connection()
try:
rows = connection.execute(
"SELECT id, title, completed FROM tasks ORDER BY id"
).fetchall()
return [row_to_task(row) for row in rows]
finally:
connection.close()
def get_task_record(task_id: int) -> dict[str, Any] | None:
connection = get_connection()
try:
row = connection.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?",
(task_id,),
).fetchone()
return row_to_task(row) if row is not None else None
finally:
connection.close()
def update_task_record(task_id: int, completed: bool) -> dict[str, Any] | None:
connection = get_connection()
try:
cursor = connection.execute(
"UPDATE tasks SET completed = ? WHERE id = ?",
(int(completed), task_id),
)
connection.commit()
if cursor.rowcount == 0:
return None
row = connection.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?",
(task_id,),
).fetchone()
return row_to_task(row) if row is not None else None
finally:
connection.close()
What should match?
Your file should contain both new helpers beneath list_task_records(). Each helper should close its connection inside a finally block.
Expose retrieval and partial updates
The API layer turns database results into typed responses. A Pydantic model validates the update body before the database receives it.
- Switch back to main.py.
- Replace the import section at the top of the file with this complete import group:
from contextlib import asynccontextmanager
from typing import Any
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
from database import (
create_task_record,
get_task_record,
initialize_database,
list_task_records,
update_task_record,
)
Why do these imports change?
The two database helpers connect the new routes to SQLite. HTTPException lets each route return a structured missing-task response.
- Save main.py.
- Watch the development-server terminal for the reload result.
You should see the server resume without an import error. The API can now access both database helpers.
- Replace the model section containing TaskCreate and Task with this complete model group:
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=100)
class TaskUpdate(BaseModel):
completed: bool
class Task(BaseModel):
id: int
title: str
completed: bool
What does the update model validate?
The TaskUpdate model requires a Boolean completed value. FastAPI uses that model to document and validate the partial update body.
- Save main.py.
- Refresh the interactive documentation in your browser.
You should see the TaskUpdate schema listed in the API documentation.
- Add the retrieval route below create_task() by pasting this code:
@app.get("/tasks/{task_id}", response_model=Task)
def get_task(task_id: int) -> Any:
task = get_task_record(task_id)
if task is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found",
)
return task
How does retrieval handle missing data?
The route returns the mapped database record when the ID exists. A missing record raises an HTTP exception with the task-specific error detail.
- Save main.py.
- Refresh http://127.0.0.1:8000/docs.
You should see a new GET /tasks/{task_id} operation in the documentation.
Prediction check: consider whether requesting task 1 should return the persisted row from the previous step.
- Expand the GET /tasks/{task_id} operation.
- Set task_id to 1.
- Submit the request.
You should see the persisted task with its ID, title, and completion state. The retrieval route now reaches the same SQLite data as the list route.
Does task 1 return a missing-task response?
Your database may have assigned a different ID if task data was reset. Execute GET /tasks to find an ID that exists.
Use Help me trace my task lookup if an existing ID still fails.
- Add the partial-update route below get_task() by pasting this code:
@app.patch("/tasks/{task_id}", response_model=Task)
def update_task(task_id: int, task: TaskUpdate) -> Any:
updated_task = update_task_record(task_id, task.completed)
if updated_task is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found",
)
return updated_task
What completes the update loop?
The route passes the validated completion state to update_task_record(). It returns the updated task or raises the same structured error used by retrieval.
- Save main.py.
- Refresh the interactive documentation.
You should now see a PATCH /tasks/{task_id} operation beside the retrieval route.
Is the PATCH operation missing?
Check that @app.patch starts at the left edge of main.py. Confirm that TaskUpdate is defined above the route.
Use Help me expose my PATCH route if the route remains missing.
✔️ Awesome, I've got everything!
Your API now exposes retrieval and completion-update operations with structured missing-task errors.
ⓧ I'd like to double check the full code
- Compare your complete main.py file with this reference:
from contextlib import asynccontextmanager
from typing import Any
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
from database import (
create_task_record,
get_task_record,
initialize_database,
list_task_records,
update_task_record,
)
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=100)
class TaskUpdate(BaseModel):
completed: bool
class Task(BaseModel):
id: int
title: str
completed: bool
@asynccontextmanager
async def lifespan(app: FastAPI):
initialize_database()
yield
app = FastAPI(title="Task API", version="1.0.0", lifespan=lifespan)
@app.get("/health")
def health_check() -> dict[str, str]:
return {"status": "ok"}
@app.get("/tasks", response_model=list[Task])
def list_tasks() -> Any:
return list_task_records()
@app.post(
"/tasks",
response_model=Task,
status_code=status.HTTP_201_CREATED,
)
def create_task(task: TaskCreate) -> Any:
return create_task_record(task.title)
@app.get("/tasks/{task_id}", response_model=Task)
def get_task(task_id: int) -> Any:
task = get_task_record(task_id)
if task is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found",
)
return task
@app.patch("/tasks/{task_id}", response_model=Task)
def update_task(task_id: int, task: TaskUpdate) -> Any:
updated_task = update_task_record(task_id, task.completed)
if updated_task is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found",
)
return updated_task
What should match?
Your file should import both database helpers and HTTPException. It should also define TaskUpdate before the two new routes.
Document and protect the repository
A root README.md gives a GitHub reviewer the context needed to run and assess your backend. Markdown keeps that documentation readable as plain text or rendered content.
- Create README.md inside the fastapi-task-api folder from the Visual Studio Code Explorer sidebar.
- Paste the project overview and feature list into the empty file:
# Persistent Task API
A small FastAPI backend that validates task data, persists it in SQLite, and exposes an interactive API contract.
## Features
- Health check
- Create and list tasks
- Retrieve a task by ID
- Mark a task complete or incomplete
- Pydantic request and response validation
- SQLite persistence across server restarts
- Structured 404 responses
## Tech stack
- Python 3.10+
- FastAPI 0.141.1
- Pydantic 2.13.5
- SQLite through Python's `sqlite3` module
What does this section communicate?
The opening states the backend's purpose. The feature list gives a reviewer observable capabilities instead of a list of source files.
- Save README.md.
You should see README.md in the Explorer sidebar with the project title and feature list in the editor.
- Add the Windows setup section below the tech stack by pasting this content:
## Run locally on Windows
Open Command Prompt in the project directory, then run:
```text
py -m venv .venv
.venv\Scripts\activate
py -m pip install -r requirements.txt
fastapi dev main.py
```
Open `http://127.0.0.1:8000/docs` to use the interactive API documentation.
Why include local setup?
This section gives a reviewer the same environment-creation and startup sequence used in the project. The documentation points directly to the interactive API contract.
- Save README.md.
You should now see a Windows setup section with the environment, installation, server, and documentation commands.
- Add the route table and backend-concepts section at the end of README.md by pasting this content:
## API routes
| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/health` | Check that the API is running |
| GET | `/tasks` | List every task |
| POST | `/tasks` | Create a task |
| GET | `/tasks/{task_id}` | Retrieve one task |
| PATCH | `/tasks/{task_id}` | Change a task's completion state |
Example create request:
```json
{
"title": "Write API documentation"
}
```
Example update request:
```json
{
"completed": true
}
```
## Backend concepts demonstrated
This project demonstrates routing, request validation, response schemas, HTTP status codes, application lifespan, SQL parameter binding, transactions, persistence, and API documentation.
What completes the README?
The route table makes the API surface scannable. The request examples show the accepted JSON shapes while the final section names the backend skills demonstrated.
- Save README.md.
You should see all five API operations plus create and update request examples in the finished README.
✔️ Awesome, I've got everything!
Your README now explains what the project does and how another Windows user can run it.
ⓧ I'd like to double check the full code
- Compare your complete README.md file with this reference:
# Persistent Task API
A small FastAPI backend that validates task data, persists it in SQLite, and exposes an interactive API contract.
## Features
- Health check
- Create and list tasks
- Retrieve a task by ID
- Mark a task complete or incomplete
- Pydantic request and response validation
- SQLite persistence across server restarts
- Structured 404 responses
## Tech stack
- Python 3.10+
- FastAPI 0.141.1
- Pydantic 2.13.5
- SQLite through Python's `sqlite3` module
## Run locally on Windows
Open Command Prompt in the project directory, then run:
```text
py -m venv .venv
.venv\Scripts\activate
py -m pip install -r requirements.txt
fastapi dev main.py
```
Open `http://127.0.0.1:8000/docs` to use the interactive API documentation.
## API routes
| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/health` | Check that the API is running |
| GET | `/tasks` | List every task |
| POST | `/tasks` | Create a task |
| GET | `/tasks/{task_id}` | Retrieve one task |
| PATCH | `/tasks/{task_id}` | Change a task's completion state |
Example create request:
```json
{
"title": "Write API documentation"
}
```
Example update request:
```json
{
"completed": true
}
```
## Backend concepts demonstrated
This project demonstrates routing, request validation, response schemas, HTTP status codes, application lifespan, SQL parameter binding, transactions, persistence, and API documentation.
What should match?
Your README should include the project summary, seven features, four technologies, Windows setup commands, five API routes, two request examples, and the backend-concepts summary.
Generated environments and database files do not belong in a source repository. A .gitignore file records which local artifacts should stay outside version control.
- Create .gitignore inside the fastapi-task-api folder from the Explorer sidebar.
- Paste these ignore rules into the empty file:
.venv/
__pycache__/
*.py[cod]
tasks.db
What do these rules exclude?
The rules exclude the local virtual environment and Python cache files. They also exclude the generated SQLite database so repository visitors create their own local data.
- Save .gitignore.
You should see .gitignore in the Explorer sidebar with four ignore patterns.
✔️ Awesome, I've got everything!
Your repository files now document the backend while keeping generated artifacts out of version control.
ⓧ I'd like to double check the full code
- Compare your complete .gitignore file with this reference:
.venv/
__pycache__/
*.py[cod]
tasks.db
What should match?
Your file should contain exactly one rule for the virtual environment, one rule for cache directories, one rule for compiled Python files, and one rule for the SQLite database.
The final walkthrough checks every documented operation before focusing on the two newest behaviors.
- Switch back to http://127.0.0.1:8000/docs.
- Execute GET /health.
You should see {"status":"ok"} in a successful response.
- Execute GET /tasks.
You should see the tasks currently persisted in tasks.db.
- Prepare a new task through POST /tasks with this request body:
{
"title": "Write API documentation"
}
What does this request prove?
The request exercises title validation and task creation. The response should include a generated ID plus a completion value of false.
- Submit the create request.
You should see the new task with a generated ID and a successful creation status.
- Execute GET /tasks/{task_id} with task_id set to 1.
You should see task 1 returned as one response object.
- Prepare PATCH /tasks/{task_id} with task_id set to 1.
- Use this request body:
{
"completed": true
}
What does this body change?
The Boolean value passes through TaskUpdate before reaching SQLite. The database stores the new state and returns the updated task.
Prediction check: decide whether task 1 should come back with its completion state changed.
- Submit the update request.
You should see task 1 with "completed": true in the response.
Prediction check: consider how the API should respond when no task matches a requested ID.
- Expand the GET /tasks/{task_id} operation.
- Set task_id to 999999.
- Submit the request.
You should see a 404 response containing {"detail":"Task not found"}.
Does the final verification fail?
If the update returns a missing-task response, execute GET /tasks to confirm that task 1 exists. If the missing ID succeeds, choose a larger ID that does not appear in the task list.
Use Help me debug my final API checks to trace the request from route to database.
That closes the full backend loop. Your API now validates requests, persists changes, retrieves individual resources, and reports missing data honestly.
Secret mission
Filter Tasks by Completion Status
Clients currently receive every persisted task from the list endpoint. Add an optional completion-status filter so they can request completed tasks, incomplete tasks, or the full list.
Clean Up Your Resources
Clean Up Your Resources
This FastAPI project runs entirely on your Windows computer with no ongoing costs. The options below cover keeping, pausing, or deleting its local resources.
Resources you used:
- A local FastAPI development server running main.py.
- The fastapi-task-api folder containing main.py, database.py, requirements.txt, README.md, and .gitignore.
- The .venv directory containing the pinned project dependencies.
- The tasks.db SQLite database containing your persisted tasks.
Keep everything running
No action is needed. Choose this option while you continue developing or demonstrating the API.
- Keep the Windows Command Prompt terminal open while you continue testing the API.
- Keep tasks.db inside fastapi-task-api to preserve your completed and incomplete tasks.
- Keep .venv inside fastapi-task-api to preserve the installed dependencies.
Pause - I'll come back to this later
Pausing stops the development server while preserving the complete local project.
- Switch back to the Windows Command Prompt terminal that is running the server.
- Press Ctrl+C to stop the development server.
- Leave the fastapi-task-api folder in place.
Your server is safely paused. Your persisted tasks remain in tasks.db.
Delete - I don't want to use this again
Deleting the full folder is permanent. The smaller reset options preserve the rest of your work.
- Switch back to the Windows Command Prompt terminal that is running the server.
- Press Ctrl+C to stop the development server.
- Close Visual Studio Code after the server stops.
- Locate the fastapi-task-api folder with the Windows file manager.
- Delete tasks.db inside fastapi-task-api if you only want to reset persisted task data.
- Delete .venv inside fastapi-task-api if you only want to rebuild the installed dependencies later.
- Delete the complete fastapi-task-api folder if you want to remove every project resource.
- Confirm that the file or folder you selected no longer appears in its previous location.
That's your cleanup handled. Any resources you kept remain available locally.
Nice Work!
Nice Work!
Outstanding work! Your repository-ready Task API now handles a complete task workflow through interactive documentation.
What you learned:
- Built a documented FastAPI backend that exposes health checks plus task creation, listing, retrieval, and completion updates.
- Defined Pydantic request and response models that reject invalid task data. Returned intentional HTTP responses for successful creation plus missing tasks.
- Replaced temporary process memory with durable SQLite storage that retains tasks across server restarts. Used parameterized SQL, explicit commits, row mapping, and connection cleanup in the database layer.
- Secret Mission: Extended GET /tasks with a validated completion-status filter that returns completed or incomplete tasks on demand.
Ready to quiz yourself?