]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✨ Add `app.frontend(check_dir="auto")`, to make local development more convenient...
authorSebastián Ramírez <tiangolo@gmail.com>
Wed, 29 Jul 2026 14:31:28 +0000 (16:31 +0200)
committerGitHub <noreply@github.com>
Wed, 29 Jul 2026 14:31:28 +0000 (16:31 +0200)
docs/en/docs/tutorial/frontend.md
fastapi/applications.py
fastapi/routing.py
pyproject.toml
tests/test_frontend.py
uv.lock

index 0dacc73ba0b43f461d6297019f8d4b0daea9f675..be1bdba1852ce5aa3b35a0e49b06faeea1833c1e 100644 (file)
@@ -106,9 +106,13 @@ Then missing frontend paths return the normal `404`.
 
 ## Check Directory { #check-directory }
 
-By default, `app.frontend()` checks that the directory exists when the app is created.
+By default, `app.frontend()` uses `check_dir="auto"`.
 
-This helps catch configuration errors early. For example, if the frontend build output directory is missing, **FastAPI** will raise an error on startup.
+When the `FASTAPI_ENV` environment variable is set to `development`, **FastAPI** only shows a warning if the frontend build output directory is missing. The [`fastapi dev` command](https://github.com/fastapi/fastapi-cli#fastapi-dev) sets this environment variable for you if it is not already set. This lets you start the backend before building or starting the frontend during development.
+
+In any other environment, **FastAPI** raises an error when the app is created. This helps catch configuration errors early before deploying an app without its frontend files.
+
+You can also set `check_dir=True` to always check the directory when the app is created.
 
 If your frontend files are created later, for example by a separate build step after the app object is created, set `check_dir=False`:
 
index cf55dada4694b826d429a944206db7f2ee57dc9e..b5fc76d1607622648ccc0eff63a6b08e65d42afa 100644 (file)
@@ -1247,13 +1247,16 @@ class FastAPI(Starlette):
             ),
         ] = "auto",
         check_dir: Annotated[
-            bool,
+            bool | Literal["auto"],
             Doc(
                 """
-                Check that the frontend directory exists when the app is created.
+                Check that the frontend directory exists when the app is created. When
+                set to `"auto"`, skip the check with a warning when `FASTAPI_ENV` is
+                `"development"`, and check it otherwise. The `fastapi dev` command
+                sets `FASTAPI_ENV` to `"development"` if it is not already set.
                 """
             ),
-        ] = True,
+        ] = "auto",
     ) -> None:
         """
         Serve a static frontend build as low-priority routes.
@@ -1285,6 +1288,9 @@ class FastAPI(Starlette):
         app.frontend("/", directory="dist")
         ```
         """
+        check_dir = routing._resolve_frontend_check_dir(
+            directory=directory, check_dir=check_dir
+        )
         self.router.frontend(
             path,
             directory=directory,
index 52973226784da6fb6d2f688e466335eab594168a..189dc15163534b46325e849270dbf244d356175f 100644 (file)
@@ -9,6 +9,7 @@ import os
 import stat
 import threading
 import types
+import warnings
 from collections.abc import (
     AsyncIterator,
     Awaitable,
@@ -1876,13 +1877,31 @@ def _get_resolved_absolute_path(path: str | os.PathLike[str]) -> str:
     return os.path.realpath(os.fspath(path))
 
 
+def _resolve_frontend_check_dir(
+    *,
+    directory: str | os.PathLike[str],
+    check_dir: bool | Literal["auto"],
+) -> bool:
+    if check_dir != "auto":
+        return check_dir
+    if os.environ.get("FASTAPI_ENV") != "development":
+        return True
+    if not os.path.isdir(directory):
+        warnings.warn(
+            f"Frontend directory '{directory}' does not exist. "
+            f"Resolved absolute path: '{_get_resolved_absolute_path(directory)}'",
+            stacklevel=3,
+        )
+    return False
+
+
 class _FrontendStaticFiles(StaticFiles):
     def __init__(
         self,
         *,
         directory: str | os.PathLike[str],
         fallback: Literal["auto", "index.html", "404.html"] | None,
-        check_dir: bool = True,
+        check_dir: bool,
     ) -> None:
         self.fallback = fallback
         if check_dir and not os.path.isdir(directory):
@@ -2025,7 +2044,7 @@ class _FrontendRoute(BaseRoute):
         *,
         directory: str | os.PathLike[str],
         fallback: Literal["auto", "index.html", "404.html"] | None = "auto",
-        check_dir: bool = True,
+        check_dir: bool,
     ) -> None:
         if fallback not in {"auto", "index.html", "404.html", None}:
             raise AssertionError(
@@ -2099,7 +2118,7 @@ class _FrontendRouteGroup(BaseRoute):
         *,
         directory: str | os.PathLike[str],
         fallback: Literal["auto", "index.html", "404.html"] | None = "auto",
-        check_dir: bool = True,
+        check_dir: bool,
     ) -> None:
         self.routes.append(
             _FrontendRoute(
@@ -2624,13 +2643,16 @@ class APIRouter(routing.Router):
             ),
         ] = "auto",
         check_dir: Annotated[
-            bool,
+            bool | Literal["auto"],
             Doc(
                 """
-                Check that the frontend directory exists when the app is created.
+                Check that the frontend directory exists when the app is created. When
+                set to `"auto"`, skip the check with a warning when `FASTAPI_ENV` is
+                `"development"`, and check it otherwise. The `fastapi dev` command
+                sets `FASTAPI_ENV` to `"development"` if it is not already set.
                 """
             ),
-        ] = True,
+        ] = "auto",
     ) -> None:
         """
         Serve a static frontend build as low-priority routes.
@@ -2664,6 +2686,9 @@ class APIRouter(routing.Router):
         app.include_router(router)
         ```
         """
+        check_dir = _resolve_frontend_check_dir(
+            directory=directory, check_dir=check_dir
+        )
         normalized_path = _normalize_frontend_path(path)
         if self._frontend_routes is None:
             self._frontend_routes = _FrontendRouteGroup(
index 2fd2f2e5d13928c253c285a1534892a4bb567fe0..dcabd9cb72c4a96fbea678b82f3071d9ec688493 100644 (file)
@@ -58,7 +58,7 @@ Changelog = "https://fastapi.tiangolo.com/release-notes/"
 
 [project.optional-dependencies]
 standard = [
-    "fastapi-cli[standard] >=0.0.8",
+    "fastapi-cli[standard] >=0.0.32",
     "fastar >= 0.9.0",
     # For the test client
     "httpx >=0.23.0,<1.0.0",
@@ -77,7 +77,7 @@ standard = [
 ]
 
 standard-no-fastapi-cloud-cli = [
-    "fastapi-cli[standard-no-fastapi-cloud-cli] >=0.0.8",
+    "fastapi-cli[standard-no-fastapi-cloud-cli] >=0.0.32",
     # For the test client
     "httpx >=0.23.0,<1.0.0",
     # For templates
@@ -95,7 +95,7 @@ standard-no-fastapi-cloud-cli = [
 ]
 
 all = [
-    "fastapi-cli[standard] >=0.0.8",
+    "fastapi-cli[standard] >=0.0.32",
     # # For the test client
     "httpx >=0.23.0,<1.0.0",
     # For templates
index d647037e7cdaf66cba0687f5f7a3efd4ade1803e..0fd899b94c5b34a3107b06adeaa70c33c766959e 100644 (file)
@@ -1176,13 +1176,50 @@ def test_check_dir_true_fails_early_for_missing_directory(monkeypatch, tmp_path:
     monkeypatch.chdir(tmp_path)
 
     with pytest.raises(RuntimeError, match="does not exist") as exc_info:
-        app.frontend("/", directory="missing")
+        app.frontend("/", directory="missing", check_dir=True)
 
     message = str(exc_info.value)
     assert "'missing'" in message
     assert str(tmp_path / "missing") in message
 
 
+def test_check_dir_auto_warns_in_development(monkeypatch, tmp_path: Path):
+    monkeypatch.setenv("FASTAPI_ENV", "development")
+    app = FastAPI()
+
+    with pytest.warns(UserWarning, match="does not exist") as warnings:
+        app.frontend("/", directory=tmp_path / "missing")
+
+    assert str(tmp_path / "missing") in str(warnings[0].message)
+    assert warnings[0].filename == __file__
+
+
+def test_check_dir_auto_router_warning_points_to_user_code(monkeypatch, tmp_path: Path):
+    monkeypatch.setenv("FASTAPI_ENV", "development")
+    router = APIRouter()
+
+    with pytest.warns(UserWarning, match="does not exist") as warnings:
+        router.frontend("/", directory=tmp_path / "missing")
+
+    assert warnings[0].filename == __file__
+
+
+def test_check_dir_true_fails_in_development(monkeypatch, tmp_path: Path):
+    monkeypatch.setenv("FASTAPI_ENV", "development")
+    app = FastAPI()
+
+    with pytest.raises(RuntimeError, match="does not exist"):
+        app.frontend("/", directory=tmp_path / "missing", check_dir=True)
+
+
+def test_check_dir_auto_fails_outside_development(monkeypatch, tmp_path: Path):
+    monkeypatch.setenv("FASTAPI_ENV", "production")
+    router = APIRouter()
+
+    with pytest.raises(RuntimeError, match="does not exist"):
+        router.frontend("/", directory=tmp_path / "missing")
+
+
 def test_check_dir_false_allows_missing_directory_and_fails_on_request(tmp_path: Path):
     app = FastAPI()
     app.frontend("/", directory=tmp_path / "missing", check_dir=False)
diff --git a/uv.lock b/uv.lock
index 5d917c9c16c677b003f7054ef6b67ddae44f247f..984408406515394242c593df9d43cbed14f88efb 100644 (file)
--- a/uv.lock
+++ b/uv.lock
@@ -1005,9 +1005,9 @@ requires-dist = [
     { name = "email-validator", marker = "extra == 'all'", specifier = ">=2.0.0" },
     { name = "email-validator", marker = "extra == 'standard'", specifier = ">=2.0.0" },
     { name = "email-validator", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=2.0.0" },
-    { name = "fastapi-cli", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.0.8" },
-    { name = "fastapi-cli", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.0.8" },
-    { name = "fastapi-cli", extras = ["standard-no-fastapi-cloud-cli"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.8" },
+    { name = "fastapi-cli", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.0.32" },
+    { name = "fastapi-cli", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.0.32" },
+    { name = "fastapi-cli", extras = ["standard-no-fastapi-cloud-cli"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.0.32" },
     { name = "fastar", marker = "extra == 'standard'", specifier = ">=0.9.0" },
     { name = "httpx", marker = "extra == 'all'", specifier = ">=0.23.0,<1.0.0" },
     { name = "httpx", marker = "extra == 'standard'", specifier = ">=0.23.0,<1.0.0" },
@@ -1144,7 +1144,7 @@ translations = [
 
 [[package]]
 name = "fastapi-cli"
-version = "0.0.20"
+version = "0.0.32"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "rich-toolkit" },
@@ -1152,9 +1152,9 @@ dependencies = [
     { name = "typer" },
     { name = "uvicorn", extra = ["standard"] },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/d3/ca/d90fb3bfbcbd6e56c77afd9d114dd6ce8955d8bb90094399d1c70e659e40/fastapi_cli-0.0.20.tar.gz", hash = "sha256:d17c2634f7b96b6b560bc16b0035ed047d523c912011395f49f00a421692bc3a", size = 19786, upload-time = "2025-12-22T17:13:33.794Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/eb/3b534c6f8e157f9ddbf2a153512307c886cad0b258739c200dd8ff8c4452/fastapi_cli-0.0.32.tar.gz", hash = "sha256:38024d2345275e1b37ce8848727a580d84901b570e96b3256d9d36a9a5039424", size = 26636, upload-time = "2026-07-16T12:16:58.678Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/08/89/5c4eef60524d0fd704eb0706885b82cd5623a43396b94e4a5b17d3a3f516/fastapi_cli-0.0.20-py3-none-any.whl", hash = "sha256:e58b6a0038c0b1532b7a0af690656093dee666201b6b19d3c87175b358e9f783", size = 12390, upload-time = "2025-12-22T17:13:31.708Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/53/56ae5ae17bb0a5d89d1d31e5320eb1865553ebbfbde91cdc4c221245f2a8/fastapi_cli-0.0.32-py3-none-any.whl", hash = "sha256:8dcc286fa32f01bbd3f65dd09cfd5a2540ed5f2230b77db7fd30978d6165f3c4", size = 14670, upload-time = "2026-07-16T12:16:57.297Z" },
 ]
 
 [package.optional-dependencies]