Skip to content

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

Install uv on your system.

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:

main.py
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 task

app = 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 dev

The 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
tests/test_main.py
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

Last updated on