Set up a FastAPI project with uv
Most FastAPI tutorials have you install Python, create a virtual environment, and pip-install three packages before you write your first endpoint. This tutorial uses uv for the entire setup: Python install, virtual environmentAn isolated folder where Python installs packages for one project, keeping them separate from other projects and your system Python. Learn more → , dependencies, and locking. By the end you will have a working API with tests and linting.
Prerequisites
Create the project
$ uv init --no-package --python 3.13 taskapi
Initialized project `taskapi` at `/path/to/taskapi`
$ cd taskapi
uv init writes the starter files for the project. --no-package keeps the project layout flat (a main.py at the top level instead of a src/ package). --python 3.13 pins the interpreterThe program that reads and executes Python code. When you run "python3 hello.py", python3 is the interpreter.
so the tutorial is reproducible regardless of what your system has installed.
-
- .python-version
- README.md
- main.py
- pyproject.toml
Open pyproject.toml:
[project]
name = "taskapi"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []Note
Without --python 3.13, uv writes whichever Python it finds newest on your machine into requires-python. On a machine with Python 3.14 installed, that produces requires-python = ">=3.14", which can later block uv python pin 3.13. Pinning at init time avoids that conflict.
Add FastAPI
$ uv add "fastapi[standard]"
Using CPython 3.13.6
Creating virtual environment at: .venv
Installed 43 packages in 55ms
+ fastapi==0.141.1
+ uvicorn==0.52.1
...
The [standard] extra bundles uvicorn (the ASGI server that runs your app), watchfiles (for auto-reload during development), and httptools. Without it, fastapi dev would not be available and you would need to install uvicorn separately.
pyproject.toml now records the dependencyAn external package your project needs, listed in pyproject.toml so tools can install it automatically.
:
dependencies = [
"fastapi[standard]>=0.141.1",
]Tip
Commit pyproject.toml and uv.lock to version control, and add .venv/ to .gitignore (uv already generates a .gitignore that covers it). The lockfileA file that records the exact version of every installed package, so everyone working on the project gets identical installs.
guarantees teammates recreate the same environment with uv sync.
Write a minimal API
Open main.py and replace its contents:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
tasks = []
class Task(BaseModel):
name: str
done: bool = False
@app.get("/tasks")
def list_tasks():
return tasks
@app.post("/tasks", status_code=201)
def create_task(task: Task):
tasks.append(task)
return taskapp = FastAPI() creates the application instance that uvicorn looks for. The Task model uses Pydantic, which FastAPI includes as a dependency, to validate incoming JSON. Two endpoints handle the basics: GET /tasks returns the current list, and POST /tasks adds a new item after validating the request body against the Task schema.
Start the development server
uv run executes the command inside the project’s virtual environment, ensuring all dependencies are installed first.
uv run fastapi devThe server starts with auto-reload enabled:
$ uv run fastapi dev
⚡️ Starting FastAPI in development mode
🐍 Using import string: main:app
🌐 Server started at http://127.0.0.1:8000
Documentation at http://127.0.0.1:8000/docs
Logs:
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28643] using WatchFiles
Visit http://127.0.0.1:8000/docs in a browser to see the interactive API documentation FastAPI generates from your endpoint signatures and Pydantic models. Try the POST /tasks endpoint from the docs page by clicking “Try it out”, entering {"name": "Buy groceries"}, and hitting “Execute”. Then call GET /tasks to confirm the item was stored.
Press CTRL+C to stop the server.
Note
fastapi dev auto-discovers main:app by scanning for a FastAPI() instance in main.py. If you rename the file or the variable, pass the import string explicitly: uv run fastapi dev myapp.py.
Add tests
FastAPI ships a TestClient that sends requests to your app without starting a real server. Add pytest as a development dependencyA package needed during development (testing, linting, formatting) that is not shipped to users of your project. Installed with the --dev flag.
:
$ uv add --dev pytest
Resolved 49 packages in 327ms
Installed 4 packages in 5ms
+ iniconfig==2.3.0
+ packaging==26.3
+ pluggy==1.6.0
+ pytest==9.1.1
The --dev flag marks pytest as a development dependency, keeping it out of production installs. httpx, which TestClient uses under the hood, is already installed because fastapi[standard] pulls it in.
Because this is a --no-package project (flat layout with main.py at the root), pytest cannot import your application module by default. Tell it where to look by adding a pythonpath setting to pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["."]Without this line, from main import app in the test file fails with ModuleNotFoundError: No module named 'main'.
Create a tests/ directory and a test file:
$ mkdir tests
from fastapi.testclient import TestClient
from main import app, tasks
client = TestClient(app)
def test_list_tasks_empty():
tasks.clear()
response = client.get("/tasks")
assert response.status_code == 200
assert response.json() == []
def test_create_task():
tasks.clear()
response = client.post("/tasks", json={"name": "Write tests"})
assert response.status_code == 201
assert response.json() == {"name": "Write tests", "done": False}TestClient(app) wraps your FastAPI application so you can call .get() and .post() on it directly. Each test calls tasks.clear() first so tests run independently regardless of execution order.
Run the tests:
$ uv run pytest tests/ -v
============================= test session starts ==============================
platform darwin -- Python 3.13.6, pytest-9.1.1, pluggy-1.6.0
rootdir: /path/to/taskapi
configfile: pyproject.toml
plugins: anyio-4.14.2
collected 2 items
tests/test_main.py::test_list_tasks_empty PASSED [ 50%]
tests/test_main.py::test_create_task PASSED [100%]
============================== 2 passed in 0.19s ===============================
Both tests pass.
Add Ruff for linting and formatting
Add Ruff as a development dependency:
$ uv add --dev ruff
Resolved 50 packages in 189ms
Installed 1 package in 2ms
+ ruff==0.16.2
Check the project for lint violations and formatting issues:
$ uv run ruff check .
All checks passed!
Both pass on the code written so far because the tutorial’s examples follow Ruff’s defaults. See Set up Ruff for formatting and checking your code for configuring rule sets and auto-fix.
Review the project structure
This flat layout works for small APIs. As a project grows, move the application code into a package:
-
- pyproject.toml
- uv.lock
-
-
- __init__.py
- main.py
- models.py
- routes.py
-
-
- test_main.py
To switch to that layout, re-initialize with uv init --package (or create the src/ tree manually and add a [build-system] table to pyproject.toml). With a package layout, uv installs your project into the venv, so from taskapi.main import app works in tests without the pythonpath workaround.
Run in production
fastapi dev enables auto-reload and binds to 127.0.0.1, both suited for local work. For production, use fastapi run:
$ uv run fastapi run main.py
⚡️ Starting FastAPI in production mode
🐍 Using import string: main:app
🌐 Server started at http://0.0.0.0:8000
Documentation at http://0.0.0.0:8000/docs
fastapi run disables auto-reload and binds to 0.0.0.0 so the server accepts connections from other machines.
Warning
0.0.0.0 accepts connections from any network interface. In production, place a reverse proxy like nginx or Caddy in front of uvicorn.
For finer control (worker count, timeouts, TLS), call uvicorn directly:
uv run uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4--workers 4 spawns four uvicorn worker processes for handling concurrent requests. See How to use uv in a Dockerfile for containerizing this setup.
Reproduce the environment
A teammate cloning the repo runs one command:
$ uv sync
Using CPython 3.13.6
Creating virtual environment at: .venv
Resolved 50 packages in 2ms
Installed 47 packages in 92ms
+ fastapi==0.141.1
+ pytest==9.1.1
+ ruff==0.16.2
+ uvicorn==0.52.1
...
uv reads .python-version and uv.lock, downloads Python 3.13 if needed, creates .venv/, and installs the same package versions you developed against. From there they have the same uv run fastapi dev and uv run pytest you do.
Next steps
- FastAPI official tutorial builds out routes, databases, authentication, and background tasks. Run every command with
uv runinstead of barepython. - Setting up testing with pytest and uv covers parametrized tests and code coverage.
- Set up Ruff for formatting and checking your code walks through Ruff’s full rule set, auto-fix, and pre-commit integration.
- Setting up GitHub Actions with uv runs tests and linting on every push.
- How to use uv in a Dockerfile containerizes a uv-managed project for deployment.
- uv’s FastAPI integration guide covers Docker deployment patterns.