Install FastAPI and Structure a Project

Published by Programming Line in FastAPI. Last updated .

FastAPI starts small: install the framework, point an ASGI server at an application object, and return JSON from a route. The lesson outcome is more durable than that first success. You will create an isolated Python environment, install the packages needed for a basic API, and structure the project as an importable package so it can grow without turning main.py into a catch-all module.

In this course, that structure matters because every later FastAPI topic depends on it: request models need a place to live, routers need clean prefixes, dependencies need predictable import paths, and tests need to import the same app object the server runs. A good foundation makes the framework feel simple because the boundaries are visible.

What FastAPI Is Running

A FastAPI project has three different layers that are easy to confuse. fastapi is the web framework. It creates the FastAPI application object, registers path operations, validates request data, serializes responses, and generates OpenAPI documentation. starlette, which FastAPI builds on, supplies the ASGI application machinery, routing primitives, middleware support, and response classes. uvicorn is the ASGI server. It opens a socket, accepts HTTP traffic, translates each request into ASGI events, calls your application, and writes the response back to the client.

The import target passed to Uvicorn is usually written as package.module:variable. For example, uvicorn app.main:app means: import the Python module app.main, then look for a variable named app inside that module. That detail explains many startup failures. Uvicorn is not searching the whole project for a FastAPI app; it is performing a normal Python import from the current working directory and then reading one named attribute.

Environment and Package Anatomy

Use a virtual environment so the project controls its dependencies instead of depending on whatever is installed globally. The common workflow is python -m venv .venv, activate it for your shell, then run python -m pip install "fastapi[standard]". The standard extra installs FastAPI with commonly used optional packages and a development server command. Installing fastapi and uvicorn separately is also fine when you want tighter dependency control.

A minimal project should be importable as a package. This means the application code sits in a directory such as app/, and that directory contains __init__.py. The file may be empty; its important job is to tell Python tooling that app is a package with stable import paths. Tests can then import from app.main import app, routers can import shared dependencies, and Uvicorn can resolve app.main:app consistently.

Path Purpose
app/main.py creates the FastAPI instance and includes routers
app/routers/ groups path operations by feature or resource
app/schemas.py defines Pydantic models for request and response shapes
app/dependencies.py contains reusable dependency functions
tests/ imports the app and verifies HTTP behavior with TestClient

Example 1: A Small Runnable App

The first example is deliberately tiny. It proves that installation, imports, the ASGI app object, routing, and JSON serialization all work. Save this as app/main.py inside a package directory that also contains app/__init__.py.

from fastapi import FastAPI

app = FastAPI(title="Course Tasks API")


@app.get("/health")
def health_check() -> dict[str, str]:
    return {"status": "ok"}

Run it with uvicorn app.main:app --reload from the project root. A request to GET /health returns {"status":"ok"}. Visiting /docs shows Swagger UI generated from the registered route. The important mechanism is that the decorator records the function as a path operation on the app instance at import time. When Uvicorn imports app.main, the decorator has already attached the route before the first request arrives.

Example 2: Move Routes Into a Router

As soon as a project has more than a few routes, put related endpoints in an APIRouter. A router is a collection of path operations that can later be included in the main application with a prefix and tags. This keeps feature code close together while preserving a single application object for the server.

from fastapi import APIRouter, FastAPI

router = APIRouter(prefix="/courses", tags=["courses"])


@router.get("/{course_slug}")
def read_course(course_slug: str) -> dict[str, str]:
    return {"slug": course_slug, "title": course_slug.replace("-", " ").title()}


app = FastAPI(title="Course Tasks API")
app.include_router(router)

With this code, GET /courses/fastapi-foundations returns {"slug":"fastapi-foundations","title":"Fastapi Foundations"}. The router prefix is joined with the route path, so /{course_slug} becomes /courses/{course_slug}. In a real project, this router would usually live in app/routers/courses.py, and app/main.py would import it and call app.include_router(courses.router). The trade-off is one more file and import, but the payoff is that adding lessons, users, or enrollments does not force unrelated code into the same module.

Example 3: Add Schemas and Dependencies

Project structure starts to pay off when endpoints need validated request bodies and reusable context. Pydantic models describe the shape of input and output data. FastAPI dependency functions describe values the endpoint needs but should not construct itself, such as a settings object, database session, or current user. The example below keeps everything in one block so it is runnable, but the model and dependency are named the way you would split them into schemas.py and dependencies.py.

from typing import Annotated

from fastapi import Depends, FastAPI
from pydantic import BaseModel, Field

app = FastAPI(title="Course Tasks API")


class TaskCreate(BaseModel):
    title: str = Field(min_length=3, max_length=80)
    priority: int = Field(default=3, ge=1, le=5)


class TaskRead(BaseModel):
    id: int
    title: str
    priority: int
    created_by: str


def current_user() -> str:
    return "instructor"


@app.post("/tasks", response_model=TaskRead, status_code=201)
def create_task(payload: TaskCreate, user: Annotated[str, Depends(current_user)]) -> TaskRead:
    return TaskRead(id=1, title=payload.title, priority=payload.priority, created_by=user)

A valid request such as {"title":"Outline routers","priority":2} returns status 201 with {"id":1,"title":"Outline routers","priority":2,"created_by":"instructor"}. If title is too short or priority is outside 1 through 5, FastAPI returns a validation error before the endpoint body runs. That behavior comes from FastAPI reading the function signature, identifying payload as a body model, resolving the Depends value, and using the response model to serialize the returned object.

Example 4: Test the Same App Object

A structured project should be testable without starting a real network server. TestClient calls the ASGI app in process, so tests exercise routing, validation, dependency resolution, and response serialization while staying fast.

from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)


def test_create_task() -> None:
    response = client.post("/tasks", json={"title": "Outline routers", "priority": 2})

    assert response.status_code == 201
    assert response.json() == {
        "id": 1,
        "title": "Outline routers",
        "priority": 2,
        "created_by": "instructor",
    }


def test_rejects_invalid_priority() -> None:
    response = client.post("/tasks", json={"title": "Bad priority", "priority": 9})

    assert response.status_code == 422

The deterministic behavior is the pair of status codes: valid input produces 201, while invalid input produces 422. The test imports app.main exactly as Uvicorn does. That symmetry catches broken imports, missing router registration, and accidental changes to the public contract.

Design Choices and Trade-offs

There is no single folder layout that fits every FastAPI service, but there are useful pressure points. A very small learning app can keep models and routes in main.py. Once features multiply, split by responsibility: routers for HTTP endpoints, schemas for public data models, dependencies for reusable request-time providers, and services for domain logic that should be testable without FastAPI.

Splitting too early can make a beginner project feel scattered. Splitting too late creates circular imports, especially when routers import app from main.py or when shared models live inside a feature module that every other feature imports. Prefer one-directional imports: main.py imports routers; routers import schemas, dependencies, and services; lower layers do not import main.py. If a lower layer needs configuration, pass it as an argument or dependency instead of importing the app object.

The --reload flag is useful during development because Uvicorn watches files and restarts the worker after changes. It should not be treated as a production setting. Reloading adds overhead and changes process behavior. For production, run a managed server process without reload, configure logging explicitly, and let deployment tooling restart the service when new code is released.

Failure Modes and Troubleshooting

Symptom: Uvicorn prints Error loading ASGI app. Cause: the import target does not match the package path, the working directory is wrong, or the variable name after the colon is not app. Diagnose: from the project root, run python -c "from app.main import app; print(app.title)". Correct: add missing __init__.py, run the command from the directory that contains app/, or change the Uvicorn target to the actual module and variable.

Symptom: ModuleNotFoundError: No module named 'app' appears in tests. Cause: tests are being run from a directory where the project root is not on Python’s import path, or the code is not packaged consistently. Diagnose: print the current directory and inspect whether app/ is a sibling of tests/. Correct: run python -m pytest from the project root and keep application code under a package directory.

Symptom: endpoints return 404 even though route functions exist. Cause: the router was defined but never included, or the request path forgot the router prefix. Diagnose: open /openapi.json and search for the expected path. Correct: call app.include_router(router) in main.py and request the full prefixed path.

Symptom: the server starts, then crashes after adding imports between routers. Cause: circular imports, commonly caused by importing app into route modules. Diagnose: read the traceback from the first module listed to the partially initialized module. Correct: move shared models or dependencies into separate modules and have main.py include routers instead of having routers mutate the app directly.

Reliability and Security Implications

Installation and layout are not only developer convenience. Pinning dependencies in a lock file or reviewed requirements file makes environments reproducible. Isolating a virtual environment prevents one project from silently using packages installed for another project. Keeping route modules small reduces the chance that import-time side effects, such as opening sockets or connecting to databases, run during tests or documentation generation.

Project structure also affects security review. Request models make validation rules visible. Dependency functions create a single place to inspect authentication, authorization, database sessions, and configuration loading. Avoid hard-coding secrets in main.py or route modules; load them through environment-aware settings and inject those settings where needed. Performance benefits are practical too: when expensive clients are created in the correct lifecycle location rather than inside every endpoint call, request latency and connection churn are easier to control.

Hands-on Lab

Prerequisites: Python is installed, you can run commands from a terminal, and you are working in an empty project directory. Create app/, app/routers/, and tests/. Add empty __init__.py files in app/ and app/routers/. Create a virtual environment with python -m venv .venv, activate it, then install dependencies with python -m pip install "fastapi[standard]" pytest.

  1. Create app/schemas.py with the TaskCreate and TaskRead models from Example 3.
  2. Create app/dependencies.py with current_user.
  3. Create app/routers/tasks.py, define an APIRouter with prefix /tasks, and move the create_task endpoint into it.
  4. Create app/main.py, instantiate FastAPI, import the tasks router, and include it.
  5. Run uvicorn app.main:app --reload from the project root.
  6. Verify GET /docs lists POST /tasks. Send {"title":"Write tests","priority":1} to POST /tasks and confirm a 201 response with created_by set to instructor.
  7. Add the tests from Example 4, adjusting imports only if your module names differ, then run python -m pytest.

Cleanup: stop Uvicorn with Ctrl+C. If this was a throwaway lab, deactivate the virtual environment and remove the project directory. If you are keeping it, record the installed dependencies in your chosen dependency file so another developer can recreate the environment.

Assessment Exercises

  • Your app runs with python app/main.py but fails with uvicorn app.main:app. Explain why direct script execution can hide package import problems and how you would restructure the command or files.
  • A teammate imports app from app.main inside app/routers/users.py so they can register routes there. Identify the likely long-term failure and rewrite the relationship using APIRouter.
  • Design a folder layout for a course API with courses, lessons, and enrollments. Decide which modules own routers, schemas, dependencies, and domain logic, and justify one import direction you would forbid.
  • A test for POST /tasks gets 404. List three concrete checks you would perform before changing endpoint code.
  • Explain when you would choose fastapi[standard] and when you would install a narrower set of packages such as fastapi plus a specific ASGI server.

Summary

Installing FastAPI is the easy first step; structuring the project is what keeps the next steps manageable. A FastAPI service runs because Uvicorn imports a named ASGI application object, FastAPI registers path operations on that object, and Python resolves everything through normal package imports. Keep main.py focused on application assembly, place feature endpoints in routers, put data contracts in schemas, and use dependencies for reusable request-time context. That foundation gives the rest of the FastAPI course a stable place to add validation, persistence, authentication, background work, and tests.