Build a Task Manager REST API
Build a CRUD REST API with FastAPI and PostgreSQL on your local machine.
Introduction
30 Second Summary
Every time you scroll through Instagram, add something to a shopping cart, or check the weather on your phone, there is an invisible layer pulling data from a database and sending it to your screen. That layer is called an API.
In this project, you will build a Task Manager REST API using Python. Your API will let any client create, read, update, and delete tasks stored in a PostgreSQL database.
What You'll Build
A REST API powered by FastAPI and PostgreSQL that exposes five endpoints for full CRUD (Create, Read, Update, Delete) operations on a task database.
By the end of this project, you'll have:
- A FastAPI application with five endpoints that create, list, retrieve, update, and delete tasks.
- A PostgreSQL database running locally with a task table that persists your data across server restarts.
- Auto-generated interactive API documentation accessible in your browser for testing every endpoint without writing any frontend code.
- Secret Mission: Add filtering and search query parameters to make your task list endpoint production-ready.
Are there any prerequisites?
You should have basic Python knowledge and be comfortable running commands in a terminal. No prior experience with databases or APIs is needed.
Create Your Database and Data Model
Your tools are installed and your virtual environment is ready. Now it's time to create the actual database that will store your tasks and write the Python code that describes what a task looks like.
Every API needs two things: a place to store data (the database) and a definition of what that data looks like (the model). In this step, you'll create both and connect them together.
In this step, get ready to:
- Create the taskmanager database in pgAdmin 4.
- Define your Task data model in main.py.
- Run the app to auto-create the database table.
Create the taskmanager database
Before your API can store tasks, it needs a dedicated database. You'll create one called taskmanager using pgAdmin 4's graphical interface.
- Return to pgAdmin 4 (the tool you connected in the previous step).
- In the left sidebar, click the arrow next to Servers to expand it.
- Expand your PostgreSQL 17 server. If prompted, enter the postgres password you set during installation.
- Right-click on Databases.
- Select Create, then Database... from the menu.
- In the Database field on the General tab, type taskmanager.
- Click Save.
The taskmanager database now appears in the left sidebar under Databases.
What is a database in this context?
A PostgreSQL server can hold many databases. Each database is an isolated container for tables and data. By creating taskmanager, you're giving your API its own dedicated space that won't interfere with other projects.
Don't see the new database?
Right-click on Databases in the sidebar and click Refresh. If you see a permissions error, make sure you're connected as the postgres superuser.
Help me troubleshoot why my database didn't appear in pgAdmin
Create main.py with your database connection and Task model
Now you'll write the Python code that connects to your new database and defines what a "task" looks like. SQLModel lets you define your data structure as a Python class, and it handles all the database communication behind the scenes.
- In VS Code, create a new file called main.py inside your task-manager-api folder (at the same level as the venv folder).
- Add the following imports and database configuration to main.py:
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- Database Configuration ---
DATABASE_URL = "postgresql://postgres:your-password-here@localhost:5432/taskmanager"
engine = create_engine(DATABASE_URL, echo=True)
What does this code do?
DATABASE_URL tells Python exactly where your database lives: the postgres user, on localhost port 5432, in the taskmanager database.
create_engine creates a connection pool to that database. The echo=True parameter prints every SQL statement to your terminal so you can see exactly what's happening behind the scenes.
- Replace your-password-here with the password you set for the postgres user during PostgreSQL installation.
- Below the database configuration, add your data models:
# --- Models ---
class TaskBase(SQLModel):
title: str
description: str | None = None
completed: bool = False
class Task(TaskBase, table=True):
id: int | None = Field(default=None, primary_key=True)
created_at: datetime = Field(default_factory=datetime.now)
class TaskCreate(TaskBase):
pass
class TaskUpdate(SQLModel):
title: str | None = None
description: str | None = None
completed: bool | None = None
Why are there four model classes?
TaskBase holds the shared fields (title, description, completed) that other models inherit from.
Task is the actual database table (table=True). It adds an auto-generated id and a created_at timestamp that the client never needs to send.
TaskCreate represents what a client sends when creating a task (just title, description, and completed status). TaskUpdate makes every field optional so clients can update just one field at a time without sending everything.
- Below the models, add the database setup and app initialization:
# --- Database Setup ---
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(app: FastAPI):
create_db_and_tables()
yield
# --- App ---
app = FastAPI(title="Task Manager API", version="1.0.0", lifespan=lifespan)
What does this code do?
create_db_and_tables() reads your model definitions and creates matching tables in PostgreSQL if they don't already exist.
The lifespan function runs create_db_and_tables() once when your app starts up. This is FastAPI's recommended pattern for startup logic. The yield keyword marks the boundary between startup and shutdown.
- Save main.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- Database Configuration ---
DATABASE_URL = "postgresql://postgres:your-password-here@localhost:5432/taskmanager"
engine = create_engine(DATABASE_URL, echo=True)
# --- Models ---
class TaskBase(SQLModel):
title: str
description: str | None = None
completed: bool = False
class Task(TaskBase, table=True):
id: int | None = Field(default=None, primary_key=True)
created_at: datetime = Field(default_factory=datetime.now)
class TaskCreate(TaskBase):
pass
class TaskUpdate(SQLModel):
title: str | None = None
description: str | None = None
completed: bool | None = None
# --- Database Setup ---
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(app: FastAPI):
create_db_and_tables()
yield
# --- App ---
app = FastAPI(title="Task Manager API", version="1.0.0", lifespan=lifespan)
Run your app to create the table
Your code is ready. When you start the server, FastAPI will call create_db_and_tables() automatically, which creates a task table in your taskmanager database.
- Start the development server by running this command in your terminal (make sure your virtual environment is still active):
fastapi dev main.py
What does this command do?
fastapi dev starts a development server at http://127.0.0.1:8000 with auto-reload enabled. Every time you save main.py, the server restarts automatically so you can test changes instantly.
- Look at your terminal output. You should see SQL statements including CREATE TABLE task being executed.
- Open your browser and go to http://127.0.0.1:8000/docs.
You should see the FastAPI interactive documentation page. It will be mostly empty for now since you haven't added any endpoints yet.
- Switch back to pgAdmin 4. Expand taskmanager, then Schemas, then Tables. You should see the task table listed.
What just happened?
When the server started, SQLModel.metadata.create_all(engine) looked at your Task model class and created a matching table in PostgreSQL with columns for id, title, description, completed, and created_at. Your Python class IS your database schema.
Seeing a connection error?
If you see an error mentioning "password authentication failed", double check that the password in your DATABASE_URL matches the one you set during PostgreSQL installation.
If you see "connection refused", make sure PostgreSQL is running. Open the Windows Services app (search for "Services" in the Start menu) and confirm postgresql-x64-17 shows a status of Running.
Help me fix my FastAPI database connection error
Your database, model, and table are all set up. Next, you'll add endpoints that let clients create and read tasks through your API.
Build Your Create and Read Endpoints
Your database is connected and your Task model is ready. Now it is time to build the endpoints that actually let clients interact with your data.
An endpoint is a specific URL that your API exposes for a specific action. You will create three endpoints in this step: one to create tasks, one to list all tasks, and one to fetch a single task by its ID.
In this step, get ready to:
- Add a POST endpoint that creates new tasks in the database.
- Add GET endpoints that retrieve all tasks or a single task by ID.
- Test your endpoints using FastAPI's auto-generated interactive documentation.
Add the Create Task endpoint (POST /tasks)
The POST HTTP method is used when a client wants to create a new resource. Your TaskCreate model from the previous step already defines what the client sends. Now you need the endpoint that receives that data and saves it to PostgreSQL.
- In main.py, scroll to the bottom of the file (below the app = FastAPI(...) line) and add the Create Task endpoint by pasting this code:
# --- Endpoints ---
@app.post("/tasks", response_model=Task)
def create_task(task: TaskCreate):
# Open a database session
with Session(engine) as session:
# Convert the client's input into a full Task object
db_task = Task.model_validate(task)
# Add the task to the session and save it
session.add(db_task)
session.commit()
# Refresh to get the auto-generated id and created_at
session.refresh(db_task)
return db_task
What does this code do?
@app.post("/tasks", response_model=Task) registers this function to handle POST requests at the /tasks URL. The response_model tells FastAPI to format the response using the full Task model (including the auto-generated fields).
Task.model_validate(task) converts the TaskCreate input into a database-ready Task object. Then session.commit() writes it to PostgreSQL, and session.refresh(db_task) pulls back the auto-generated id and created_at values so they appear in the response.
- Save main.py. The dev server will auto-reload with your changes.
- Open your browser to http://127.0.0.1:8000/docs. You should see the POST /tasks endpoint listed in the Swagger UI.
- Click on POST /tasks to expand it.
- Click the Try it out button on the right side.
- Replace the example JSON body with this:
{
"title": "Buy groceries",
"description": "Milk, eggs, and bread"
}
- Click Execute.
- You should see a 200 response with the full task object including an auto-generated id and created_at timestamp.
Not seeing the POST /tasks endpoint?
Check that your terminal is not showing an error. A common cause is indentation issues in the new code. Make sure the def create_task line is at the top level (not indented inside another function).
Also confirm you pasted the code below the app = FastAPI(...) line, not above it. The app must exist before you can attach endpoints to it.
Help me debug why my POST endpoint is not appearing in the Swagger docs.
Add the Read Tasks endpoints (GET /tasks and GET /tasks/{task_id})
Creating tasks is only useful if you can retrieve them later. You need two GET endpoints: one that returns all tasks and one that returns a specific task by its ID.
- In main.py, add the Get All Tasks endpoint below the create_task function by pasting this code:
@app.get("/tasks", response_model=list[Task])
def read_tasks():
# Query all tasks from the database
with Session(engine) as session:
tasks = session.exec(select(Task)).all()
return tasks
What does this code do?
session.exec(select(Task)).all() runs a SQL query that selects every row from the task table and returns them as a Python list. The response_model=list[Task] tells FastAPI the response will be a JSON array of task objects.
- Below the read_tasks function, add the Get Single Task endpoint by pasting this code:
@app.get("/tasks/{task_id}", response_model=Task)
def read_task(task_id: int):
# Look up a single task by its primary key
with Session(engine) as session:
task = session.get(Task, task_id)
# Return 404 if the task does not exist
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
What does this code do?
The {task_id} in the URL path is a path parameter. FastAPI automatically extracts it and passes it to your function as task_id: int.
session.get(Task, task_id) fetches a task by its primary key. If no task exists with that ID, it returns None, and your code raises a 404 error so the client gets a clear "Task not found" message.
- Save main.py.
- Refresh http://127.0.0.1:8000/docs in your browser. You should now see three endpoints listed: POST /tasks, GET /tasks, and GET /tasks/{task_id}.
- Expand GET /tasks, click Try it out, then click Execute. You should see the task you created earlier in the response.
- Expand GET /tasks/{task_id}, click Try it out, enter 999 in the task_id field, and click Execute. You should get a 404 response with the detail "Task not found".
Seeing an Internal Server Error?
Check your terminal for the full error traceback. A common issue is a missing blank line between the end of one function and the decorator of the next. Python needs these functions to be clearly separated.
Also make sure you are importing select from sqlmodel at the top of your file. It should already be there from the previous step.
Help me debug my GET endpoints returning an Internal Server Error.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py and that all three endpoints appear in your Swagger UI at /docs.
ⓧ I'd like to double check the full code
Here is the complete main.py file at this point. Compare it to yours and fix any differences.
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- Database Configuration ---
DATABASE_URL = "postgresql://postgres:your-password-here@localhost:5432/taskmanager"
engine = create_engine(DATABASE_URL, echo=True)
# --- Models ---
class TaskBase(SQLModel):
title: str
description: str | None = None
completed: bool = False
class Task(TaskBase, table=True):
id: int | None = Field(default=None, primary_key=True)
created_at: datetime = Field(default_factory=datetime.now)
class TaskCreate(TaskBase):
pass
class TaskUpdate(SQLModel):
title: str | None = None
description: str | None = None
completed: bool | None = None
# --- Database Setup ---
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(app: FastAPI):
create_db_and_tables()
yield
# --- App ---
app = FastAPI(title="Task Manager API", version="1.0.0", lifespan=lifespan)
# --- Endpoints ---
@app.post("/tasks", response_model=Task)
def create_task(task: TaskCreate):
with Session(engine) as session:
db_task = Task.model_validate(task)
session.add(db_task)
session.commit()
session.refresh(db_task)
return db_task
@app.get("/tasks", response_model=list[Task])
def read_tasks():
with Session(engine) as session:
tasks = session.exec(select(Task)).all()
return tasks
@app.get("/tasks/{task_id}", response_model=Task)
def read_task(task_id: int):
with Session(engine) as session:
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
Test the Create and Read flow
You have all three endpoints working. Now create a few more tasks so you can verify the full flow and confirm that data persists in PostgreSQL.
- In /docs, expand POST /tasks and click Try it out.
- Create a second task with the title "Write project report" and click Execute.
- Create a third task with the title "Schedule team meeting" and click Execute.
- Expand GET /tasks, click Try it out, then Execute. You should see all three tasks in the response array.
- Expand GET /tasks/{task_id}, click Try it out, enter 1 in the task_id field, and click Execute. You should see just your first task.
Now verify that your data survives a server restart. This proves the data lives in PostgreSQL, not just in memory.
- In your terminal, press Ctrl+C to stop the server.
- Restart the server by running:
fastapi dev main.py
- Go back to http://127.0.0.1:8000/docs, use GET /tasks again, and confirm all your tasks are still there.
Why do tasks survive a restart?
Your tasks are stored in the PostgreSQL database on disk, not in your Python application's memory. When you stop and restart the server, FastAPI reconnects to the same database and reads the same rows. This is exactly why real applications use databases instead of in-memory variables.
Your API can now create and read tasks. Next up, you will add the ability to update and delete them, completing the full CRUD pattern.
Build Your Update and Delete Endpoints
You can create tasks and read them back from the database. But a real task manager needs the ability to modify tasks (mark them complete, fix a typo in the title) and remove tasks you no longer need.
In this step, you will add the final two CRUD operations: Update and Delete. These endpoints complete your REST API so that any client can perform every standard data operation.
In this step, get ready to:
- Add a PATCH endpoint that updates individual fields on a task.
- Add a DELETE endpoint that removes a task from the database.
- Test error handling for missing and invalid data.
Add the Update Task endpoint (PATCH /tasks/{task_id})
When a client wants to update a task, they should only need to send the fields they want to change. If they just want to mark a task as completed, they should not have to resend the title and description too.
Your TaskUpdate model (already defined in main.py) makes all fields optional for exactly this reason.
- In main.py, scroll to the bottom of the file and find your read_task function.
- Add the following PATCH endpoint below it:
@app.patch("/tasks/{task_id}", response_model=Task)
def update_task(task_id: int, task_update: TaskUpdate):
with Session(engine) as session:
# Look up the task by its ID
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
# Extract only the fields the client actually sent
task_data = task_update.model_dump(exclude_unset=True)
# Apply those fields to the existing task object
task.sqlmodel_update(task_data)
session.add(task)
session.commit()
session.refresh(task)
return task
What does this code do?
This endpoint handles partial updates. The key technique is model_dump(exclude_unset=True). It converts the request body into a dictionary but only includes fields the client actually sent.
If the client sends {"completed": true}, the dictionary will only contain completed. The title and description remain untouched. Then sqlmodel_update() applies just those fields to the existing database record.
- Save main.py. The dev server auto-reloads your changes.
- Switch to your browser and go to http://127.0.0.1:8000/docs.
- Expand the new PATCH /tasks/{task_id} section and click Try it out.
- Enter the ID of one of your existing tasks in the task_id field.
- In the request body, enter {"completed": true} and click Execute.
- You should see a 200 response with the task showing "completed": true while the title and description remain unchanged.
Seeing a 422 or 500 error?
- A 422 error means the request body is not valid JSON. Make sure you have the curly braces and double quotes correct.
- A 500 error usually means a typo in your Python code. Check the terminal running your server for a traceback pointing to the exact line.
- Make sure you used the exact function name update_task and that the @app.patch decorator is on the line directly above the function definition.
Why is my PATCH endpoint returning a 500 or 422 error?
Add the Delete Task endpoint (DELETE /tasks/{task_id})
The Delete endpoint is simpler than Update. It finds the task, removes it from the database, and returns a confirmation message.
- In main.py, add the following DELETE endpoint below your update_task function:
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
with Session(engine) as session:
# Find the task or return 404
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
# Remove the task from the database
session.delete(task)
session.commit()
return {"message": "Task deleted successfully"}
What does this code do?
This endpoint uses session.delete(task) to remove the record from PostgreSQL. After committing, the task no longer exists in the database.
Notice this endpoint does not declare a response_model. It returns a plain dictionary with a success message instead of a Task object, since the task no longer exists after deletion.
- Save main.py.
- In your browser at /docs, expand DELETE /tasks/{task_id} and click Try it out.
- Enter the ID of one of your test tasks and click Execute.
- You should see a 200 response with {"message": "Task deleted successfully"}.
- Confirm it is gone by using GET /tasks/{task_id} with the same ID. You should get a 404 response.
Task not deleting?
- Make sure you have session.commit() after session.delete(task). Without the commit, the deletion is never saved to the database.
- Check that the task_id you entered matches an existing task. Use GET /tasks first to see which IDs are available.
Why is my DELETE endpoint not removing the task from the database?
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py and that both new endpoints appear in your /docs page.
ⓧ I'd like to double check the full code
Here is the complete main.py with all five endpoints:
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- Database Configuration ---
DATABASE_URL = "postgresql://postgres:your-password-here@localhost:5432/taskmanager"
engine = create_engine(DATABASE_URL, echo=True)
# --- Models ---
class TaskBase(SQLModel):
title: str
description: str | None = None
completed: bool = False
class Task(TaskBase, table=True):
id: int | None = Field(default=None, primary_key=True)
created_at: datetime = Field(default_factory=datetime.now)
class TaskCreate(TaskBase):
pass
class TaskUpdate(SQLModel):
title: str | None = None
description: str | None = None
completed: bool | None = None
# --- Database Setup ---
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
@asynccontextmanager
async def lifespan(app: FastAPI):
create_db_and_tables()
yield
# --- App ---
app = FastAPI(title="Task Manager API", version="1.0.0", lifespan=lifespan)
# --- Endpoints ---
@app.post("/tasks", response_model=Task)
def create_task(task: TaskCreate):
with Session(engine) as session:
db_task = Task.model_validate(task)
session.add(db_task)
session.commit()
session.refresh(db_task)
return db_task
@app.get("/tasks", response_model=list[Task])
def read_tasks():
with Session(engine) as session:
tasks = session.exec(select(Task)).all()
return tasks
@app.get("/tasks/{task_id}", response_model=Task)
def read_task(task_id: int):
with Session(engine) as session:
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
@app.patch("/tasks/{task_id}", response_model=Task)
def update_task(task_id: int, task_update: TaskUpdate):
with Session(engine) as session:
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
task_data = task_update.model_dump(exclude_unset=True)
task.sqlmodel_update(task_data)
session.add(task)
session.commit()
session.refresh(task)
return task
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
with Session(engine) as session:
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
session.delete(task)
session.commit()
return {"message": "Task deleted successfully"}
Test edge cases
A well-built API handles bad input gracefully. FastAPI gives you automatic validation from your model definitions, and the HTTPException calls you wrote handle missing resources.
- In /docs, try PATCH /tasks/999 (a task ID that does not exist). Click Execute.
- You should see a 404 response with "Task not found".
- Try DELETE /tasks/999. Click Execute.
- You should see the same 404 response.
- Now try POST /tasks with an empty body {} (no title provided). Click Execute.
- You should see a 422 validation error. FastAPI automatically rejects the request because title is a required field in your TaskCreate model.
Why does 422 happen automatically?
You did not write any code to check for a missing title. FastAPI uses your SQLModel class definitions as validation schemas. Because title: str in TaskBase has no default value, FastAPI knows it is required and rejects requests that omit it.
The 422 response includes a detail field listing exactly which fields failed validation and why. This is free input validation from your model definitions.
Your API now handles every standard operation and responds with clear error messages when something goes wrong. Next up, you will run the complete API end-to-end and explore the auto-generated documentation.
Run Your Complete API End-to-End
Your API now has all five CRUD endpoints. But how do you know the data is actually persisting in PostgreSQL and not just floating in memory?
In this step, you will restart your server to prove data survives a shutdown, explore FastAPI's auto-generated documentation, and call your API from the terminal the same way a real frontend would.
In this step, get ready to:
- Restart the server and confirm tasks persist.
- Explore the Swagger UI and ReDoc documentation pages.
- Call your API from the terminal using curl.
Restart and verify persistence
A database-backed API stores data independently from the running server process. If you restart the server and your tasks are still there, it proves the data lives in PostgreSQL, not in your Python process's memory.
- Switch back to the terminal where your FastAPI server is running.
- Stop the server by pressing Ctrl+C.
- Restart the server by running this command:
fastapi dev main.py
- Open your browser to http://127.0.0.1:8000/docs.
- Expand the GET /tasks endpoint, click Try it out, then click Execute.
You should see all the tasks you created in previous steps returned in the response body. They survived the server restart because they live in your PostgreSQL database.
Why does this matter?
Many beginner tutorials use in-memory lists to store data. Those disappear the moment the server stops. Your API uses a real database, which means your data persists across restarts, crashes, and deployments.
This is exactly how production APIs work. The server is stateless. The database holds the state.
Tasks list is empty?
Make sure you are connecting to the same taskmanager database. Check that your DATABASE_URL in main.py still points to localhost:5432/taskmanager with your correct password.
Help me figure out why my tasks are missing after restarting the server.
Explore the auto-generated documentation
FastAPI automatically generates interactive documentation from your code. It reads your endpoint definitions, type hints, and model classes to produce a full reference that stays in sync with your code.
- In your browser, visit http://127.0.0.1:8000/docs.
This is the Swagger UI page. You should see all five of your endpoints listed with their HTTP methods, paths, request body schemas, and response schemas.
- Now visit http://127.0.0.1:8000/redoc for an alternative documentation view.
ReDoc shows the same information in a cleaner, read-only layout. Both pages are generated from the exact same source: your Python code.
Why is auto-generated documentation valuable?
Manually writing API docs is tedious and they go stale quickly. Because FastAPI generates docs directly from your type annotations, they are always accurate. If you change your model, the docs update automatically on the next server restart.
Any frontend developer who needs to connect to your API can open /docs and immediately see every endpoint, what to send, and what they will get back.
Verify your API from the terminal
The Swagger UI is great for testing, but real clients (web apps, mobile apps, other services) call your API over HTTP. You can simulate this with curl, a command-line tool for making HTTP requests.
- Open a new terminal window. Keep the FastAPI server running in your original terminal.
- Call your API by running this command:
curl http://127.0.0.1:8000/tasks
You should see a JSON array of your tasks printed directly in the terminal. This is the exact same response a Flutter app or React frontend would receive when calling your API.
What just happened?
Your terminal sent an HTTP GET request to your local server. FastAPI received it, queried PostgreSQL, and returned the task list as JSON. This is the same flow that powers every app on your phone that loads data from a server.
Seeing 'connection refused' or no response?
Make sure your FastAPI server is still running in the other terminal. You need two terminal windows: one running the server, and one sending the curl request.
Help me troubleshoot why curl cannot connect to my FastAPI server.
Secret mission
Add Filtering and Search
Your API returns all tasks every time. Real APIs let clients ask for exactly what they need. Can you add query parameters that filter by completion status and search by title?
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 cloud costs.
Resources you used:
- The taskmanager database in PostgreSQL.
- The task-manager-api project folder (includes your virtual environment and code).
- PostgreSQL 17 running as a Windows service.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend your API soon.
- Your PostgreSQL service continues running in the background. It uses minimal resources when idle.
- Your task-manager-api folder stays on your Desktop with the virtual environment intact.
- To start developing again, open a terminal in your project folder, activate the virtual environment with venv\Scripts\activate, and run fastapi dev main.py.
Pause - I'll come back to this later
Stop the running processes to free up memory, but keep all your files and data in place.
- Stop the FastAPI dev server by pressing Ctrl+C in the terminal where it is running.
- Stop the PostgreSQL service by opening the Windows Services app (search for "Services" in the Start menu), finding postgresql-x64-17, right-clicking it, and selecting Stop.
- To resume later, start the PostgreSQL service again from the same Services app, then activate your virtual environment and run fastapi dev main.py.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to clean up completely.
- Stop the FastAPI dev server by pressing Ctrl+C in the terminal.
- Drop the database. Open a terminal and run:
psql -U postgres -c "DROP DATABASE taskmanager;"
- Enter your postgres superuser password when prompted.
- Delete the project folder. Open a terminal and run:
cd ~/Desktop
rmdir /s /q task-manager-api
- Uninstall PostgreSQL 17. Open Windows Settings, select Apps, then Installed apps. Find PostgreSQL 17 in the list, click the three-dot menu, and select Uninstall. This also removes pgAdmin 4.
Nice Work!
Nice Work!
You just built a production-style REST API from scratch. Your FastAPI application connects to a real PostgreSQL database and exposes five fully functional endpoints that any frontend application could use to manage tasks.
You've learned how to:
- Build a complete REST API with FastAPI that performs Create, Read, Update, and Delete operations using proper HTTP methods.
- Connect Python to PostgreSQL using SQLModel and persist data across server restarts.
- Design data models with separate base, create, and update schemas to handle request validation and partial updates.
- Secret Mission: Added query parameter filtering and search to make your API production-ready.
Ready to quiz yourself?