diff --git a/.claude/agents/refactor-cleaner.md b/.claude/agents/refactor-cleaner.md index 3287b60..70d6f8d 100644 --- a/.claude/agents/refactor-cleaner.md +++ b/.claude/agents/refactor-cleaner.md @@ -178,6 +178,6 @@ def test_old_auth_flow(): # dead: tests removed feature When working in this FastAPI/DDD codebase: - Respect domain boundaries (auth/, users/, etc.) - Keep cross-cutting concerns in shared/ only if used by 3+ domains -- Preserve the separation between api/ (representation) and core/ (business logic) +- Preserve the separation between http/ (representation) and core/ (business logic) - Don't remove code from RBAC, caching, or retry mechanisms without careful verification - Keep diffs minimal, scope strictly to cleanup, no unrelated changes diff --git a/.claude/skills/backend-patterns/SKILL.md b/.claude/skills/backend-patterns/SKILL.md index 17a6d0d..8ac23b0 100644 --- a/.claude/skills/backend-patterns/SKILL.md +++ b/.claude/skills/backend-patterns/SKILL.md @@ -9,7 +9,7 @@ description: DDD structure, caching, auth, error handling, and common backend pa ### DDD Structure - **Domain Modules**: Each domain (e.g., `auth/`, `users/`) contains models (SQLTable), schemas (Request/Response), services. -- **Representation Layer**: `api/` handles HTTP requests, routing, controllers. +- **Representation Layer**: `http/` handles HTTP requests, routing, controllers. - **Core Layer**: `core/` contains business-related domains. ### Shared Module Guidelines @@ -54,7 +54,7 @@ async def handler(cache: CacheProtocol = Depends(get_cache)): All responses wrapped in `{code, msg, data}`: ```python -from src.responses import Response +from src.http import Response # Return raw data (middleware wraps) return [{"id": 1}] diff --git a/README.md b/README.md index 80fb3f2..c47b5d4 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ This repo also provides a full-featured, best-practiced backend template for bui This repo follows **Domain-Driven Design (DDD)** principles to structure the codebase for better maintainability and scalability: - **Domain Modules**: Each domain (e.g., `auth/`, `users/`) has its own module containing models(SQLTable), schemas (Request/Response), services. -- **Representation Layer**: `api/` module handles HTTP requests, routing, and controllers. You can add `grpc`, `graphql` in this layer as needed. +- **Representation Layer**: `http/` module handles HTTP requests, routing, and controllers. You can add `grpc`, `graphql` in this layer as needed. - **Core Layer**: `core/` module contains business-related domains. The `shared/` module contains **cross-cutting concerns** used by multiple domains. Before adding code to `shared/`, it must meet these criteria: @@ -167,7 +167,7 @@ All JSON responses automatically wrapped in `{code, msg, data}` format: ```python from fastapi import APIRouter -from src.responses import Response +from src.http import Response router = APIRouter() diff --git a/src/http/__init__.py b/src/http/__init__.py new file mode 100644 index 0000000..1082050 --- /dev/null +++ b/src/http/__init__.py @@ -0,0 +1,4 @@ +from .handlers import register_exception_handlers +from .response import Response + +__all__ = ["Response", "register_exception_handlers"] diff --git a/src/handlers.py b/src/http/handlers.py similarity index 98% rename from src/handlers.py rename to src/http/handlers.py index 51fbdf5..573b68d 100644 --- a/src/handlers.py +++ b/src/http/handlers.py @@ -5,7 +5,7 @@ from fastapi.responses import JSONResponse from src.exceptions import BusinessException -from src.responses.base import Response +from .response import Response from src.shared.errors import error_code_to_http_status logger = logging.getLogger(__name__) diff --git a/src/logging.py b/src/http/logging.py similarity index 100% rename from src/logging.py rename to src/http/logging.py diff --git a/src/responses/middleware.py b/src/http/middleware.py similarity index 99% rename from src/responses/middleware.py rename to src/http/middleware.py index 941721e..b497c74 100644 --- a/src/responses/middleware.py +++ b/src/http/middleware.py @@ -8,7 +8,7 @@ from starlette.middleware.base import BaseHTTPMiddleware from starlette.types import ASGIApp -from .base import Response +from .response import Response class ResponseWrapperMiddleware(BaseHTTPMiddleware): diff --git a/src/responses/base.py b/src/http/response.py similarity index 100% rename from src/responses/base.py rename to src/http/response.py diff --git a/src/api/__init__.py b/src/http/routers/__init__.py similarity index 100% rename from src/api/__init__.py rename to src/http/routers/__init__.py diff --git a/src/api/auth.py b/src/http/routers/auth.py similarity index 100% rename from src/api/auth.py rename to src/http/routers/auth.py diff --git a/src/api/core.py b/src/http/routers/core.py similarity index 100% rename from src/api/core.py rename to src/http/routers/core.py diff --git a/src/api/oauth.py b/src/http/routers/oauth.py similarity index 100% rename from src/api/oauth.py rename to src/http/routers/oauth.py diff --git a/src/main.py b/src/main.py index 7ed7a2e..46709e1 100644 --- a/src/main.py +++ b/src/main.py @@ -4,12 +4,12 @@ from fastapi_pagination import add_pagination from starlette.middleware import Middleware -from src.api import auth_router, core_router, create_oauth_router +from src.http.routers import auth_router, core_router, create_oauth_router from src.cache import close_cache, init_cache from src.config import get_settings -from src.handlers import register_exception_handlers -from src.logging import setup_logging -from src.responses.middleware import ResponseWrapperMiddleware +from src.http.handlers import register_exception_handlers +from src.http.logging import setup_logging +from src.http.middleware import ResponseWrapperMiddleware from .session import close_db, init_db diff --git a/src/responses/__init__.py b/src/responses/__init__.py deleted file mode 100644 index b800297..0000000 --- a/src/responses/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .base import Response -from .middleware import ResponseWrapperMiddleware - -__all__ = ["Response", "ResponseWrapperMiddleware"] diff --git a/tests/conftest.py b/tests/conftest.py index 6afe4d3..4b2eba9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ def base_app() -> FastAPI: @pytest.fixture def app_with_middleware(base_app: FastAPI) -> FastAPI: - from src.responses import ResponseWrapperMiddleware + from src.http.middleware import ResponseWrapperMiddleware base_app.add_middleware(ResponseWrapperMiddleware) return base_app @@ -50,7 +50,7 @@ def app_with_middleware(base_app: FastAPI) -> FastAPI: @pytest.fixture def app_with_handlers(base_app: FastAPI) -> FastAPI: - from src.handlers import register_exception_handlers + from src.http.handlers import register_exception_handlers register_exception_handlers(base_app) return base_app @@ -58,8 +58,8 @@ def app_with_handlers(base_app: FastAPI) -> FastAPI: @pytest.fixture def full_app(base_app: FastAPI) -> FastAPI: - from src.responses import ResponseWrapperMiddleware - from src.handlers import register_exception_handlers + from src.http.middleware import ResponseWrapperMiddleware + from src.http.handlers import register_exception_handlers base_app.add_middleware(ResponseWrapperMiddleware) register_exception_handlers(base_app) diff --git a/tests/integration/test_audit_rbac.py b/tests/integration/test_audit_rbac.py index fea7e19..f997d21 100644 --- a/tests/integration/test_audit_rbac.py +++ b/tests/integration/test_audit_rbac.py @@ -110,10 +110,10 @@ async def superuser_user(test_db, rbac_data): async def rbac_audit_client( test_db, local_cache, admin_user, regular_user, superuser_user ): - from src.api import auth_router + from src.http.routers import auth_router from src.cache import get_cache - from src.handlers import register_exception_handlers - from src.responses.middleware import ResponseWrapperMiddleware + from src.http.handlers import register_exception_handlers + from src.http.middleware import ResponseWrapperMiddleware from src.session import get_session app = FastAPI() diff --git a/tests/integration/test_pagination.py b/tests/integration/test_pagination.py index 83d4c52..c069148 100644 --- a/tests/integration/test_pagination.py +++ b/tests/integration/test_pagination.py @@ -13,8 +13,8 @@ @pytest.fixture async def pagination_client(test_db, local_cache, test_user): from src.cache import get_cache - from src.handlers import register_exception_handlers - from src.responses.middleware import ResponseWrapperMiddleware + from src.http.handlers import register_exception_handlers + from src.http.middleware import ResponseWrapperMiddleware app = FastAPI() app.add_middleware(ResponseWrapperMiddleware) diff --git a/tests/integration/test_rbac.py b/tests/integration/test_rbac.py index e929f8b..1b46c76 100644 --- a/tests/integration/test_rbac.py +++ b/tests/integration/test_rbac.py @@ -114,10 +114,10 @@ async def superuser_user(test_db, rbac_data): @pytest.fixture async def rbac_client(test_db, local_cache, admin_user, regular_user, superuser_user): - from src.api import auth_router + from src.http.routers import auth_router from src.cache import get_cache - from src.handlers import register_exception_handlers - from src.responses.middleware import ResponseWrapperMiddleware + from src.http.handlers import register_exception_handlers + from src.http.middleware import ResponseWrapperMiddleware from src.session import get_session app = FastAPI() diff --git a/tests/integration/test_responses.py b/tests/integration/test_responses.py index af2b692..e1c59a2 100644 --- a/tests/integration/test_responses.py +++ b/tests/integration/test_responses.py @@ -74,7 +74,7 @@ async def test_endpoint(): async def test_middleware_leaves_already_wrapped_response( app_with_middleware: FastAPI, ): - from src.responses.base import Response + from src.http.response import Response @app_with_middleware.get("/api/already-unified") async def already_unified_endpoint(): diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 56398f1..e80a4a7 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -5,7 +5,7 @@ from src.shared.errors import ErrorCode from src.exceptions import BusinessException -from src.handlers import register_exception_handlers +from src.http.handlers import register_exception_handlers def test_register_all_handlers(base_app: FastAPI): diff --git a/tests/responses/__init__.py b/tests/test_http/__init__.py similarity index 100% rename from tests/responses/__init__.py rename to tests/test_http/__init__.py diff --git a/tests/responses/test_base.py b/tests/test_http/test_base.py similarity index 91% rename from tests/responses/test_base.py rename to tests/test_http/test_base.py index d51e739..15a294d 100644 --- a/tests/responses/test_base.py +++ b/tests/test_http/test_base.py @@ -17,7 +17,7 @@ def test_response_success_factory_sets_fields( factory_kwargs, expected_code, expected_msg, expected_data ): - from src.responses.base import Response + from src.http.response import Response response = Response.success(**factory_kwargs) @@ -46,7 +46,7 @@ def test_response_success_factory_sets_fields( def test_response_error_factory_sets_fields( factory_kwargs, expected_code, expected_msg, expected_data ): - from src.responses.base import Response + from src.http.response import Response response = Response.error(**factory_kwargs) @@ -57,7 +57,7 @@ def test_response_error_factory_sets_fields( def test_response_model_dump(): """Test Response.model_dump() returns correct dictionary.""" - from src.responses.base import Response + from src.http.response import Response response = Response.success(data={"test": "data"}, msg="OK", code=200) dumped = response.model_dump() @@ -71,7 +71,7 @@ def test_response_model_dump(): def test_response_generic_types(): """Test Response with different generic types.""" - from src.responses.base import Response + from src.http.response import Response response_str = Response.success(data="string data") assert response_str.data == "string data" @@ -88,7 +88,7 @@ def test_response_generic_types(): def test_response_with_none_data(): """Test Response when data is explicitly None.""" - from src.responses.base import Response + from src.http.response import Response response = Response.success(data=None) assert response.data is None diff --git a/tests/responses/test_middleware.py b/tests/test_http/test_middleware.py similarity index 97% rename from tests/responses/test_middleware.py rename to tests/test_http/test_middleware.py index 4813b6b..b8c7467 100644 --- a/tests/responses/test_middleware.py +++ b/tests/test_http/test_middleware.py @@ -7,7 +7,7 @@ from fastapi.responses import JSONResponse, PlainTextResponse from httpx import ASGITransport, AsyncClient -from src.responses.middleware import ResponseWrapperMiddleware +from src.http.middleware import ResponseWrapperMiddleware @pytest.mark.asyncio